Classes
How Python finds names (scopes), how classes and instances really work, what self is, plus inheritance, "private" names and dataclasses, explained for someone who knows Java's OOP.
You will learn
- Names vs objects, namespaces, and the LEGB scope rule, with
globalandnonlocal. - Defining classes,
__init__,self, and bound methods. - Class variables vs instance variables, and the shared-mutable trap.
- Inheritance,
super(), multiple inheritance and the MRO. - Name mangling (
__x), special methods like__repr__, properties, and@dataclass.
A word about names and objects
Objects have identity, and several names can be bound to the same object. This is called aliasing. It doesn't matter for immutable objects (numbers, strings, tuples), but it matters a lot for mutable ones. You saw it with lists in Chapter 2; it is the same for your own objects.
The upside: passing an object to a function is cheap, because only a reference is passed. And if the function modifies the object, the caller sees the change.
Scopes and namespaces
A namespace is a mapping from names to objects. Examples are the built-in names (len, print), a module's global names, and a function call's local names. Names in different namespaces never clash: fibo.total and report.total can coexist.
A scope is a region of code where a namespace is directly reachable. When Python looks up a bare name, it searches in the LEGB order:
| Letter | Scope | Example |
|---|---|---|
| Local | the current function | a variable assigned inside the function |
| Enclosing | any outer functions (for nested functions) | rate in make_tax from Chapter 5 |
| Global | the module's top level | constants and functions defined in the file |
| Built-in | Python's built-ins | len, print, ValueError |
The key rule: assigning to a name inside a function makes it local to that function, unless you declare it global (module level) or nonlocal (the enclosing function).
do_localcreates its own localspam, so the outer one is untouched.do_nonlocalrebindsscope_test'sspam.do_globalrebinds the module-levelspam.scope_test's localspamis unchanged, which is why the third line still showsnonlocal spam.
โ UnboundLocalError
โ Return the new value instead
๐ก Tip: global works, but it makes code harder to test. Prefer passing values in and returning them, or keep the state in an object, which is what classes are for.
โ Java / ๐จ JS: if, for and while blocks do not create a new scope in Python. A variable assigned inside a loop is still visible after it. Only functions, classes, modules and comprehensions create scopes.
A first look at classes
A class is a blueprint for objects. Here is a minimal class, then a useful one:
What is self?
self is the instance the method was called on. Python passes it automatically: lunch.with_tax() is shorthand for Expense.with_tax(lunch). It is Java's this, except that Python makes it an explicit first parameter. The name self is only a convention, but always use it.
โ Forgetting self
โ Store state on self
โ Java: there is no field declaration list. Attributes come into existence when they are assigned, normally in __init__. You can even add attributes to an instance later (lunch.note = "x"), though good style keeps them all in __init__. There is no method overloading and no new keyword.
Class and instance variables
Instance variables (self.name = โฆ) belong to one object. Class variables (assigned in the class body) are shared by all instances, like Java static fields. When you read obj.attr, Python looks on the instance first, then on the class.
โ ๏ธ The shared-mutable trap: a mutable class variable, such as a list, is shared by every instance. This is the same bug as the mutable default argument in Chapter 5.
โ One list for all dogs
โ Create it in __init__
Assigning through an instance (d.kind = "wolf") creates an instance attribute that hides the class one for that instance only. The class value is unchanged.
Special ("dunder") methods
Methods with double underscores on both sides, called dunder methods (like __init__), let your objects work with Python's built-in syntax. The three you'll write most often:
| Java habit | Python special method |
|---|---|
toString() | __str__ (and __repr__) |
equals() / hashCode() | __eq__ / __hash__ |
compareTo() | __lt__ (and the others, or use functools.total_ordering) |
size(), get(i) | __len__ for len(obj), __getitem__ for obj[i] |
Iterable | __iter__ (Chapter 12) |
โ ๏ธ Pitfall: if you define __eq__ but not __hash__, Python sets __hash__ to None, and your objects can no longer go into sets or be dict keys. Dataclasses (below) can handle this for you with frozen=True.
Properties, class methods and static methods
Python doesn't use getters and setters by default: just use plain attributes. If you later need logic, such as validation, turn the attribute into a property. Callers keep writing obj.amount, unchanged.
(@property is a decorator: a function that wraps the function defined on the next line. You'll use decorators like this long before you need to write your own.)
Inheritance
Put the base class in parentheses. A derived class inherits everything from it, and can override methods. super() calls the parent's version.
- Every class ultimately inherits from
object. - All methods are "virtual":
self.describe()inside a base method calls the subclass override. - Use
isinstance(obj, Cls)for type checks, nottype(obj) == Cls, so that subclasses are accepted too.
Multiple inheritance and the MRO
A class can have several bases: class C(A, B):. Python looks up attributes using the Method Resolution Order (MRO), which is roughly: the class itself, then its bases left to right, with each class appearing only once and always before its parents.
Notice that super() in Left called Right, not Base. super() means "the next class in the MRO of the actual object". In practice, multiple inheritance is mostly used for small mixins: classes that add one feature, like JsonMixin or ReprMixin.
โ Java: there are no interfaces. Python relies on duck typing ("if it walks like a duckโฆ"): any object with the right methods works. For formal contracts, there are abc.ABC abstract base classes and typing.Protocol (Appendix A).
Private variables
Python has no private keyword. Instead, there are conventions:
_name(one underscore): "internal, please don't touch". Nothing enforces it, but everyone respects it, and tools warn about it.__name(two underscores, at most one trailing): name mangling. Python renames it to_ClassName__name, so that subclasses don't clash with it by accident. It is meant to avoid accidents, not to provide security.
Odds and ends: dataclasses
Many classes are just a bundle of named fields. @dataclass generates __init__, __repr__ and __eq__ from type-annotated fields, like Java's record.
๐ก When to use what: use a dict for loose, JSON-like data; a tuple or namedtuple for small fixed records; a @dataclass for named records with defaults; and a regular class when there is real behaviour or invariants to protect.
More examples
1. A bank account with validation
2. A container class with len() and indexing
3. Polymorphism without interfaces
4. A frozen dataclass as a dict key
5. Counting instances with a class variable
Recap
- Names are looked up in LEGB order. Assigning inside a function makes a name local;
globalandnonlocaloverride that. class C:plus__init__(self, โฆ).obj.m()meansC.m(obj). State lives onself.- Class variables are shared, so never use a mutable one for per-object data.
class Sub(Base):withsuper()andisinstance(). Multiple inheritance follows the MRO._xmeans internal and__xis mangled. Add__repr__/__eq__, use@propertyfor computed or validated attributes, and@dataclassfor records.
Exercises
Exercise 1 ๐ข easy build it
Write a class Rectangle with width and height and the methods area() and perimeter().
Show solution
Every method takes self first and reads the attributes through it.
Exercise 2 ๐ข easy fix the bug
This raises TypeError: greet() takes 0 positional arguments but 1 was given. Fix it.
Show solution
Calling obj.greet() passes obj as the first argument, so the method must accept it.
Exercise 3 ๐ข easy predict the output
What does this print?
Show solution
5 10 10. a.x = 5 created an instance attribute on a, which hides the class attribute. b has no instance attribute of its own, so it sees the class's current value.
Exercise 4 ๐ข easy build it
Add a __repr__ to the Rectangle from Exercise 1, so that Rectangle(3, 4) shows as Rectangle(width=3, height=4).
Show solution
Containers show their items with repr(), so __repr__ is the one that matters for debugging. (Or just use @dataclass.)
Exercise 5 ๐ก medium predict the output
What does this print?
Show solution
changed then global. inner looks up x in the enclosing scope when it runs, and by then it is "changed". The module-level x is never touched.
Exercise 6 ๐ก medium refactor it
Rewrite with @dataclass:
Show solution
The dataclass generates the same three methods from the annotated fields.
Exercise 7 ๐ก medium build it ยท playground
In playground/models.py, create a dataclass Expense(amount: float, category: str, note: str = ""). In __post_init__, reject amounts โค 0 and lower-case the category. Add a to_dict() method and a from_dict() classmethod for JSON storage.
Show solution
__post_init__ is the dataclass hook for validation. cls(**data) unpacks the dict into keyword arguments (Chapter 5).
Exercise 8 ๐ก medium explain why
A teammate writes getAmount() and setAmount() methods for every field "like in Java". Why is that unnecessary in Python?
Show solution
In Python you can start with a plain attribute (e.amount) and later swap in a @property without changing any caller. Java needs getters up front because changing a field to a method later breaks every caller. Python doesn't have that problem.
Exercise 9 ๐ด hard predict the output
What is the MRO of D, and what does D().who() return?
Show solution
['D', 'C', 'B', 'A', 'object'] DCBA. The bases are taken left to right (C before B), and A comes after both of its subclasses. Each super() moves to the next class in D's MRO.
Exercise 10 ๐ด hard fix the bug
Instances of this class can't be put in a set: TypeError: unhashable type: 'Tag'. Why? Give two fixes.
Show solution
Defining __eq__ sets __hash__ to None, because equal objects must have equal hashes and Python can't guess one. Only hash values that never change, which is why frozen=True helps.
Exercise 11 ๐ด hard mini-project ยท playground
In playground/budget_oop.py, build a Budget class. It holds a limit per category and a list of Expense dataclass objects. Give it add(expense), which raises OverBudgetError (a custom exception) if adding the expense would exceed its category's limit; a remaining(category) method; __len__; and a __repr__. Add a subclass SoftBudget that only prints a warning instead of raising.
Show solution
The subclass overrides one method and reuses everything else. type(self).__name__ makes __repr__ correct for subclasses without changing it.
Official docs
- 9. Classes (9.1โ9.7: names, scopes, classes, inheritance, private variables, odds and ends)
- dataclasses
- Data model: special method names