Python Class Declaration: Syntax and Practical Use
python class declaration: Learn how to declare classes in Python correctly: syntax, __init__, attributes, methods, type hints, dataclasses, and common pitfalls.
When you write a python class declaration, you are defining a blueprint for objects that bundle data and behavior. The syntax is straightforward, but the way Python resolves attributes and methods has consequences for how you structure your code. This article walks through the core declaration patterns, explains the runtime behavior behind them, and points out mistakes that commonly appear in real codebases.
The Basic Class Declaration
A minimal class declaration uses the class keyword followed by a name and a colon. The class body can be empty, but you usually add attributes and methods.
class Customer: pass
That pass is required because Python expects an indented block. The class name follows the CapWords convention, which is not enforced but is the accepted style. You can instantiate the class with Customer(), and each instance is a separate object with its own namespace.
The declaration itself does not create any instance data. It defines what the instances will look like once you add attributes and methods. The class statement is executed at import time, so the class object is created when the module loads, not when you instantiate it.
Instance Attributes and the init Method
Most classes need to initialize instance-specific data. The __init__ method is the constructor hook that Python calls after the instance is created. It receives self, which is the instance being initialized.
class Customer: def __init__(self, name: str, tier: str = "standard"): self.name = name self.tier = tier
Here, self.name and self.tier become instance attributes. Every Customer object gets its own copies. The __init__ method does not return anything; assigning a value to self is the way to attach data to the instance.
If you do not define __init__, Python uses a default that takes no arguments. That means Customer() works, but Customer("Alice") raises a TypeError. The signature of __init__ determines how you construct the object.
Class Attributes vs Instance Attributes
A common point of confusion is the difference between attributes defined directly in the class body and those assigned to self.
class Customer: plan = "standard" # class attribute def __init__(self, name: str): self.name = name # instance attribute
plan is shared by all instances. If you read customer.plan, Python first looks for an instance attribute named plan. If it does not find one, it falls back to the class attribute. Assigning customer.plan = "premium" creates a new instance attribute that shadows the class attribute for that object only. Other instances still see the class value.
This behavior is useful for constants or defaults that should be shared, but it can cause subtle bugs if you mutate a mutable class attribute. For example, a list as a class attribute is shared, and modifying it through one instance affects all others. If you need per-instance mutable data, always initialize it in __init__.
Methods: Instance, Class, and Static
Methods are functions defined inside the class body. The first parameter determines the type of method.
- Instance methods take
selfas the first parameter and can access instance and class state. - Class methods take
clsand receive the class itself, not the instance. They are declared with@classmethod. - Static methods take neither
selfnorclsand are declared with@staticmethod. They behave like plain functions but live in the class namespace.
class Customer: region = "US" def __init__(self, name: str): self.name = name def display(self): return f"{self.name} ({self.region})" @classmethod def set_region(cls, region: str): cls.region = region @staticmethod def validate_name(name: str) -> bool: return bool(name.strip())
The instance method display uses self to access both instance and class data. The class method set_region changes the class attribute for all instances. The static method validate_name does not depend on the instance or class; it is just a helper grouped with the class.
Choosing the right method type keeps the class declaration clear. Use instance methods for behavior that depends on instance state, class methods for behavior that affects the class as a whole, and static methods for utility functions that are conceptually related to the class.
Using Type Hints in Class Declarations
Type hints are optional but they make class declarations more readable and help static analysis tools catch errors. You can annotate attributes in __init__ and method return types.
class Order: def __init__(self, order_id: int, items: list[str]) -> None: self.order_id = order_id self.items = items def total(self) -> float: return sum(item.price for item in self.items)
Annotations do not enforce anything at runtime. They are metadata that tools like mypy and IDE linters use. For classes with many attributes, annotating self attributes directly is not possible in the class body; you must assign them in __init__ with annotations. Alternatively, you can use variable annotations in the class body for class-level attributes.
Type hints become especially valuable when you use dataclasses, as they drive the generated __init__ signature.
Dataclasses for Declarative Class Definitions
For classes that mainly store data, the dataclasses module removes boilerplate. A dataclass declaration uses a decorator and type-annotated class attributes.
from dataclasses import dataclass @dataclass class Product: sku: str name: str price: float in_stock: bool = True
This declaration automatically generates __init__, __repr__, __eq__, and other methods based on the annotated fields. The default value True makes in_stock optional in the constructor. The generated __init__ takes parameters in the order the fields are declared.
Dataclasses are a form of class declaration that emphasizes data over custom logic. If you need custom initialization or validation, you can define __post_init__ to run after the generated __init__. This keeps the declaration concise while still allowing control.
Inheritance and Method Resolution Order
A class declaration can inherit from one or more base classes. The syntax is class Derived(Base):. Inheritance affects attribute lookup and method resolution.
class BaseCustomer: def greeting(self): return "Hello" class PremiumCustomer(BaseCustomer): def greeting(self): return "Welcome, premium customer"
When you call greeting on a PremiumCustomer instance, Python finds the method on PremiumCustomer first. If it were not defined there, Python would look in BaseCustomer. This is the method resolution order (MRO). For multiple inheritance, Python uses the C3 linearization algorithm to determine the order. You can inspect the MRO with ClassName.__mro__.
Inheritance is useful when you have a clear is-a relationship. However, deep inheritance hierarchies make class declarations harder to reason about. Prefer composition or dataclasses for data-heavy structures unless you genuinely need polymorphic behavior.
Common Mistakes in Class Declarations
One frequent mistake is mutating a mutable default in __init__. If you write def __init__(self, items=[]), the default list is created once at function definition time and shared across all instances. The correct pattern is to use None and create a new list inside.
class Order: def __init__(self, items: list[str] | None = None): self.items = items if items is not None else []
Another mistake is forgetting to call super().__init__() in a derived class. If the base class defines __init__, the derived class must explicitly call it to initialize base attributes.
class Base: def __init__(self, value: int): self.value = value class Derived(Base): def __init__(self, value: int, extra: str): super().__init__(value) self.extra = extra
Without the super() call, self.value will not exist, and you will get an AttributeError later.
Performance and Memory: slots
By default, every Python instance has a __dict__ that stores its attributes. This makes attribute access flexible but uses memory. If you have many instances of a class with a fixed set of attributes, you can declare __slots__ to prevent the creation of __dict__ and save memory.
class Point: __slots__ = ("x", "y") def __init__(self, x: float, y: float): self.x = x self.y = y
With __slots__, instances no longer have a __dict__, and attribute access is slightly faster because the attribute locations are fixed. The tradeoff is that you cannot add new attributes to an instance. Also, classes with __slots__ do not support some features like weakref unless you explicitly include __weakref__ in the slots.
Use __slots__ when you have a large number of instances and memory usage matters. For typical application classes, the flexibility of __dict__ is usually worth the overhead. Measure your actual memory footprint before optimizing, because the gains vary by workload.
A class declaration is more than syntax; it is a decision about how data and behavior are organized. Understanding attribute resolution, method types, and the tradeoffs of dataclasses and __slots__ lets you write classes that are both correct and maintainable in production.