Python Class: Attributes, Methods, and Inheritance
python **class**: Learn how to define and use Python classes: attributes, methods, inheritance, properties, dataclasses, and performance considerations.
A Python class is the primary tool for bundling data and behavior into a single object. The syntax is simple, but the runtime behavior around attributes, methods, and inheritance has nuances that affect maintainability and performance. This article covers the core mechanics of python class definitions, common patterns, and where they can go wrong.
Defining a Class and Its Instance Attributes
The most basic class definition uses the class keyword followed by a name and a colon. Inside, you typically define an __init__ method to initialize instance attributes.
class Product: def __init__(self, name: str, price: float): self.name = name self.price = price
Here, self refers to the instance being created. Each Product object gets its own name and price attributes. This is the standard way to attach data to an instance. The __init__ method is called automatically when you instantiate the class, so you can rely on it to set up a consistent initial state.
Instance Methods and the self Parameter
Methods are functions defined inside a class that operate on an instance. The first parameter is always self, which gives the method access to the instance's attributes and other methods.
class Product: def __init__(self, name: str, price: float): self.name = name self.price = price def discounted_price(self, discount: float) -> float: return self.price * (1 - discount)
Calling product.discounted_price(0.1) passes the instance as self automatically. This is how Python binds the method to the object. Without self, the method would not know which instance's data to use. The explicit self parameter is a deliberate design choice that makes attribute access unambiguous.
Class Attributes vs. Instance Attributes
Attributes defined directly in the class body are shared across all instances. They are useful for constants or default values that should not be duplicated per instance.
class Product: tax_rate = 0.2 # class attribute def __init__(self, name: str, price: float): self.name = name self.price = price
Every Product instance shares the same tax_rate object. If you assign to self.tax_rate, you create an instance attribute that shadows the class attribute. This behavior is often a source of confusion. Use class attributes for immutable values that never change per instance. For mutable defaults, you must be careful, as discussed later.
Inheritance and Method Resolution Order
Inheritance lets a child class reuse and extend the behavior of a parent class. The child class can override methods and add new attributes.
class Book(Product): def __init__(self, name: str, price: float, author: str): super().__init__(name, price) self.author = author def discounted_price(self, discount: float) -> float: # Books get an extra 5% off return super().discounted_price(discount) * 0.95
The super() call invokes the parent's method, which avoids duplicating the initialization logic. Python resolves method lookups using the Method Resolution Order (MRO), which is a linearization of the inheritance hierarchy. For single inheritance, the MRO is straightforward: the child class first, then the parent. For multiple inheritance, the MRO follows the C3 linearization algorithm, which ensures a consistent order. You can inspect the MRO with ClassName.__mro__.
Properties for Controlled Attribute Access
Sometimes you need to validate or transform an attribute when it is read or written. The @property decorator turns a method into a read-only attribute, and its setter allows controlled assignment.
class Product: def __init__(self, name: str, price: float): self._price = price @property def price(self) -> float: return self._price @price.setter def price(self, value: float): if value < 0: raise ValueError("Price cannot be negative") self._price = value
Using a property keeps the public interface unchanged while adding validation logic. This is preferable to exposing a raw attribute and scattering checks across the codebase. Properties also allow you to make attributes read-only by omitting the setter, which is useful for immutable values.
Dataclasses to Reduce Boilerplate
The dataclasses module, introduced in Python 3.7, generates __init__, __repr__, and comparison methods automatically. This is a practical way to define classes that primarily hold data.
from dataclasses import dataclass @dataclass class Product: name: str price: float tax_rate: float = 0.2
This single decorator eliminates the repetitive __init__ method. You can still add custom methods and properties. Dataclasses also support field-level defaults and type hints, which improves readability and tooling support. Use them for value objects, DTOs, or any class where the main job is storing data.
Using slots to Save Memory
By default, each instance has a __dict__ that stores its attributes. This dictionary provides flexibility but consumes memory and slows attribute access. For classes with many instances, you can define __slots__ to declare a fixed set of attributes.
class Product: __slots__ = ("name", "price") def __init__(self, name: str, price: float): self.name = name self.price = price ```n With `__slots__`, instances no longer have a `__dict__`, so they use less memory and attribute access is faster. However, you lose the ability to add new attributes dynamically. This tradeoff is acceptable for high-performance scenarios like processing millions of records. If you use inheritance, each class must define its own `__slots__`, and the parent's slots are inherited automatically. ## Common Pitfalls: Mutable Defaults and Shared State A classic mistake is using a mutable default value in a method signature. Because default values are evaluated once at function definition time, all instances share the same mutable object. ```python class ShoppingCart: def __init__(self, items=[]): # problematic self.items = items
Every ShoppingCart instance will share the same list. If one cart adds an item, all carts see it. The correct pattern is to use None and create a new list inside the method.
class ShoppingCart: def __init__(self, items=None): self.items = items if items is not None else []
This same issue applies to class attributes that are mutable. A list or dict defined at the class level is shared. If you need a per-instance copy, assign it in __init__. Understanding where state lives is critical for avoiding subtle bugs in production code.
When to Choose a Plain Class Over a Dataclass
Dataclasses are convenient, but they are not always the right choice. If your class requires complex initialization logic, custom __init__ behavior, or heavy inheritance hierarchies, a regular class gives you full control. Dataclasses also generate __eq__ and __hash__ based on fields, which may not match your intent. For example, if you want identity-based equality, a regular class is simpler. Use dataclasses when the class is primarily a data container and you want to minimize boilerplate. Use a regular class when behavior is the primary focus or when you need to customize construction.
Performance Implications of Attribute Lookup
Attribute access in Python goes through several layers: instance dictionary, class dictionary, and descriptor protocol. __slots__ reduces this overhead by replacing the instance dictionary with a fixed set of descriptors. This makes attribute access faster and memory usage lower. However, the speed difference is often small unless you are in a tight loop with millions of accesses. Measure your specific use case before optimizing. The maintainability cost of __slots__—losing dynamic attributes—may outweigh the performance gain for most applications. Use it deliberately, not as a default.
Runtime Behavior of Class and Instance Methods
Beyond self, Python also supports @classmethod and @staticmethod. A classmethod receives the class as the first argument, while a staticmethod receives no special first argument. Classmethods are useful for alternative constructors, such as Product.from_dict(). Staticmethods are for utility functions that are conceptually related to the class but do not need instance or class data. Understanding these distinctions helps you choose the right tool for the job and avoids passing unused parameters.
Final Technical Consideration: Attribute Name Mangling
When you define an attribute with two leading underscores (e.g., __secret), Python performs name mangling by prepending _ClassName. This is not true privacy; it is a mechanism to avoid accidental overrides in subclasses. It is often misunderstood as a security feature. In practice, use a single underscore to indicate internal use and rely on conventions. Name mangling can complicate debugging and is rarely necessary in well-designed code. Keep this in mind when designing class interfaces for public APIs.