Classes
Defining your own types with attributes, methods, and dataclasses
A class bundles data and behavior. Every value in Python is an instance of some class — 42 is an int, "hello" is a str, [1, 2, 3] is a list. Classes let you define your own types to model the problem you're solving.
Why classes matter: the real world
Classes are how you model entities in your domain:
- Web frameworks — Django models, Flask views, FastAPI request/response objects
- Data pipelines — Pandas DataFrame methods, SQLAlchemy table definitions
- Games — Player, Enemy, Item, Inventory classes
- APIs — User, Order, Payment, Invoice objects
- Configuration — Settings classes, feature flags, environment wrappers
Every major Python library uses classes to organize behavior and state. Understanding classes is essential for reading and writing idiomatic Python.
Etymology: why 'class'?
The word "class" comes from classification — the practice of grouping things by shared characteristics. Linnaeus used classes in biological taxonomy (Kingdom, Phylum, Class, Order...). In programming, a class is a template for creating objects that share structure and behavior.
A minimal class
class Dog: defines a new type. Dog() creates an instance of that type. The class body can be empty (pass), but usually you add an __init__ method.
The __init__ method
__init__ is the initializer (not "constructor" — the object already exists when __init__ runs). It sets up a new instance.
When you call Dog("Rex", 4), Python:
- Creates a new, empty
Doginstance. - Calls
__init__(d, "Rex", 4)to initialize it. - Returns the initialized instance.
What is 'self'?
self is just a convention — the name is not special. The first parameter of an instance method always receives the instance, and by convention we name it self. You could call it this or me or obj, but everyone uses self, so you should too.
Instance attributes vs class attributes
An instance attribute belongs to a single object. A class attribute is shared by all instances.
Mutable class attributes are a gotcha
If a class attribute is a mutable object (list, dict, set), all instances share that single object. Modifying it in one instance affects all others. This is almost never what you want.
```python class Bad: items = [] # shared by all instances!
a = Bad() b = Bad() a.items.append(1) print(b.items) # [1] — oops! ```
Always initialize mutable per-instance data in `init`.
Methods
A method is a function defined inside a class. The first parameter is always the instance (self), and Python passes it automatically when you call instance.method().
When you write d.bark(), Python translates it to Dog.bark(d) — the instance is passed as the first argument.
@classmethod and @staticmethod
Sometimes you want a method that operates on the class itself, or a utility function that logically belongs to the class but doesn't need access to instances.
@classmethodreceives the class as the first argument (cls). Useful for alternative constructors.@staticmethodreceives nothing automatic. It's just a function that lives in the class namespace for organizational reasons.
When to use @classmethod
Use @classmethod for alternative constructors — methods that create instances in different ways. Examples: dict.fromkeys(...), datetime.fromtimestamp(...), json.loads(...) (conceptually). The pattern is @classmethod returns cls(...).
Properties: computed attributes
Use @property to define a method that looks like an attribute.
Properties are useful when you want to compute a value on-the-fly or add logic (validation, logging) when reading/writing an attribute.
Dunder methods: special behavior
Methods with double underscores ("dunder") define how instances interact with Python operators and built-in functions.
| Dunder | Purpose | Triggered by |
|---|---|---|
__init__ | Initialize instance | MyClass(...) |
__repr__ | Unambiguous string | repr(obj), REPL display |
__str__ | User-friendly string | str(obj), print(obj) |
__eq__, __lt__, ... | Comparisons | a == b, a < b |
__len__ | Length | len(obj) |
__iter__ | Iteration | for x in obj |
__getitem__ | Indexing | obj[key] |
__call__ | Make instance callable | obj() |
__repr__ vs __str__
__repr__should be unambiguous and ideally show how to reconstruct the object. Target audience: developers.__str__should be readable and friendly. Target audience: end users.
If you only define one, define __repr__. str(obj) falls back to repr(obj) if __str__ is missing.
Defining __eq__ without __hash__
If you define __eq__, Python sets __hash__ to None, making instances unhashable (can't be dict keys or set elements). If you want hashable instances, define both __eq__ and __hash__, or use @dataclass(frozen=True).
@dataclass: the easy mode
Writing __init__, __repr__, and __eq__ for every data-holding class is tedious. @dataclass generates them from type-annotated attributes.
@dataclass generates:
__init__that takesxandyas arguments__repr__that showsPoint(x=3, y=4)__eq__that compares all fields
Use @dataclass for simple records
If your class is mostly "a bundle of named fields", use @dataclass. It's less boilerplate and more readable than writing __init__ by hand. For complex behavior, a regular class is fine.
Multi-file challenges
Open account.py and implement the BankAccount class with:
__init__(self, owner, balance=0)— Store owner and balance.deposit(amount)— Add to balance, return new balance. RaiseValueErrorif amount is negative.withdraw(amount)— Subtract from balance, return new balance. RaiseValueErrorif amount is negative or exceeds balance.
main.py uses your class. Do not edit main.py.
You'll implement two classes across two files:
product.py: Define Product with __init__(self, name, price) and a __repr__.
cart.py: Define ShoppingCart with:
__init__(self)— Initialize an empty list of items.add(product)— Append a product.total()— Return sum of all product prices.
main.py uses both. Do not edit main.py.
Open geometry.py and define a @dataclass named Point with two float fields: x and y.
main.py uses your class. Do not edit main.py.
Define a class Counter with:
- An
__init__(self, start=0)constructor that stores the starting value inself.value. - A method
increment(step=1)that addssteptoself.valueand returns the new value. - A method
reset()that setsself.valueback to 0.
Multiple choice questions
What is the role of self in instance methods?
It is a keyword that Python requires in method definitions.
It is the conventional name for the first parameter, which receives the instance.
It refers to the class, not the instance.
It is automatically defined; you do not need to include it in method signatures.
What does @dataclass generate for you?
Only __init__.
__init__, __repr__, and __eq__ (and optionally __hash__ if frozen=True).
Runtime type validation for all fields.
Private attributes for all fields.
What is the difference between __repr__ and __str__?
They are synonyms; Python 3 renamed __str__ to __repr__.
__repr__ is unambiguous for developers; __str__ is user-friendly.
__repr__ is for built-in types; __str__ is for user classes.
__str__ is automatically generated; __repr__ must be written by hand.
When should you use @classmethod instead of a regular method?
When the method does not need access to self.
When the method needs to access the class itself, often for alternative constructors.
When the method modifies a class attribute.
When the method is private.
Why is defining __eq__ without __hash__ a problem?
Python will raise a SyntaxError.
Instances become unhashable and cannot be used as dict keys or in sets.
__eq__ comparisons will always return False.
The class becomes abstract and cannot be instantiated.
What happens if a class attribute is a mutable object (e.g., a list) and you modify it in one instance?
Each instance gets its own copy of the list.
The change is visible in all instances because they share the same list object.
Python raises a TypeError.
The class attribute becomes an instance attribute.
Inheritance lets one class build on another. That is next.