Chapter 11 ยท Object-Oriented Python

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 global and nonlocal.
  • 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:

LetterScopeExample
Localthe current functiona variable assigned inside the function
Enclosingany outer functions (for nested functions)rate in make_tax from Chapter 5
Globalthe module's top levelconstants and functions defined in the file
Built-inPython's built-inslen, 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).

โŒ 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 habitPython 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.

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:

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; global and nonlocal override that.
  • class C: plus __init__(self, โ€ฆ). obj.m() means C.m(obj). State lives on self.
  • Class variables are shared, so never use a mutable one for per-object data.
  • class Sub(Base): with super() and isinstance(). Multiple inheritance follows the MRO.
  • _x means internal and __x is mangled. Add __repr__/__eq__, use @property for computed or validated attributes, and @dataclass for 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