7 Design subclasses right
This chapter explains how to design subclasses that behave correctly and predictably in Python. It highlights when to override versus overload methods, how dynamic typing and optional type hints affect those choices, and why coding to interfaces and relying on polymorphism leads to flexible designs. The discussion then broadens to substitutability (Liskov Substitution Principle), selecting is-a versus has-a relationships, favoring composition over inheritance to avoid hardcoded behavior, introducing factories to defer concrete choices to runtime, and applying Programming by Contract carefully with class hierarchies.
Overriding is used to give subclasses specialized behavior while preserving a superclass’s interface, with runtime polymorphism selecting the right method. Because Python’s signatures don’t include parameter types, type hints won’t prevent an unintended override and can lead to runtime errors if misused—static type checkers can help catch these issues early. Method overloading isn’t built into Python in the traditional sense, so it’s emulated with runtime parameter inspection or improved via multimethods combined with type hints to provide clear, maintainable overloads (for example, dispatching a line-length calculation on different argument shapes and types).
The Liskov Substitution Principle is illustrated by an anti-example: subclassing list for a circular buffer enables inherited operations that corrupt invariants; the fix is to aggregate a list internally (has-a) and expose only valid operations. A toy-domain case shows trade-offs: pure inheritance duplicates or hardcodes behaviors, multiple inheritance shares code but still hardcodes choices, while composition (delegation to PlayAction and Sound objects) achieves flexibility, code sharing, loose coupling, and easier evolution. Factory functions/classes further decouple clients from concrete types, enabling creation based on runtime parameters while “coding to the interface.” Finally, when using contracts with subclasses, ensure subclass preconditions are no stronger and postconditions no weaker than those of the superclass, or expose contract checks so callers validate the correct rules for the actual dynamic type.
Methods _cost() and _print_cost() in each of the subclass OrganicItem override the corresponding methods in superclass Item with the same signature.
This version of the application uses is-a relationships between the Toy subclasses and their superclass. Two design faults are the duplicated code, which violates the Don’t Repeat Yourself Principle, and the hardcoded play action and sound behavior of each toy.
The play action and sound behaviors are each a separate class. Each Toy subclass has an is-a relationship with the Toy superclass and an is-a relationships with the play action and sound classes. By sharing these classes, we’ve eliminated some code duplication, but each toy still has hardcoded behaviors.
Each Toy subclass uses an is-a relationship to the Toy superclass. The Toy superclass uses has-a relationships to the PlayAction and Sound classes, which allows the Toy subclasses to access and share play action and sound behaviors. This design is more flexible and has loose coupling between the toys and their play action and sound behaviors. The toy behaviors are no longer hardcoded in the source code but can be specified whenever a toy object is created at run time. A toy’s behaviors can also be modified at run time by calling the sound and play setter methods.
Cost 4 satisfies the superclass’s precondition but violates the subclass’s precondition.
Days 20 satisfies the subclass’s postcondition but violates the superclass’s postcondition.
Summary
- A well-designed application should contain only well-designed subclasses that support good design principles.
- The signature of a function consists of its name and the number, datatypes, and order of its parameters. It does not include the names of its parameters.
- A method of a subclass overrides a method with the same signature in its superclass. At run time, we want to call the overriding function if the object was instantiated from the subclass.
- Overloaded functions have the same name but otherwise different signatures. In a well-designed application, a set of overloaded functions have operations that are conceptually similar, the same, or equivalent.
- It is important to understand when to use the is-a (inheritance) and the has-a (aggregation) relationship between classes.
- The Liskov Substitution Principle says that if subclasses are designed properly with the is-a relationship, then a subclass object can substitute for a superclass object in the code. The program will still be logically correct and continue to run. A subclass that violates this principle can cause the program to run with logic errors or crash.
- The Favor Composition over Inheritance Principle says that there are software design situations, where using the has-a relationship instead of the is-a relationship results in classes and subclasses that are more flexible, better encapsulated against changes, less complex, and more loosely coupled.
- The has-a relationship can prevent hardcoding behaviors in the source code of classes. It provides the flexibility to set object behaviors when creating the objects at run time and to later modify the behaviors.
- A factory function encapsulates object creation. It prevents hardcoding by giving us the flexibility to determine at run time what objects to create.
- The pre- and postconditions of the methods of a superclass and its subclasses must be carefully designed so the conditions aren’t violated when those methods are called on the superclass and subclass objects.
FAQ
When should I override a method versus overload it?
Override when a subclass needs behavior that is similar to, but not exactly the same as, its superclass. The overriding method has the same name and compatible parameters and can optionally call super() to extend base behavior. Overload when multiple operations with the same conceptual meaning share a name but accept different inputs. In Python, true overloading isn’t built in, so you either dispatch at runtime (default parameters, *args, isinstance checks) or use a library such as multimethod.
What counts as a “method signature” in Python, and how does that affect overriding?
Unlike statically typed languages, Python does not include types in the runtime notion of a signature. Practically, methods with the same name are considered compatible for overriding if they match by name and arity (number of parameters). Parameter names don’t matter for overriding, but the behavioral contract should remain compatible to preserve substitutability.
How does super() relate to method overriding?
In an overriding method, calling super().method(...) lets a subclass reuse and extend the superclass’s implementation. This is common when the base logic is still valid, and the subclass only needs to adjust or augment it (for example, computing a base value then applying a subclass-specific adjustment).
Do type hints prevent accidental or incompatible overrides?
No. Type hints are ignored at runtime. A subclass can override a method with different annotations, and Python will still dispatch to the subclass, which can cause runtime errors if arguments don’t match the new expectations. Use a static type checker (e.g., mypy, Pyright/Pylance) to catch such mismatches and keep overridden method contracts compatible.
How do I implement method overloading in pure Python without external libraries?
Use one or more of these patterns:
- Defaulted or optional parameters (e.g., arg2=None) and branch at runtime.
- *args/**kwargs with explicit checks (number and types) and clear error messages for unsupported calls.
- Delegate from a single public method to private helpers per variant to keep code readable.
Keep overloaded variants semantically consistent and document accepted argument shapes.
What does the multimethod library provide, and when should I use it?
multimethod adds multiple-dispatch by type annotations: you define several methods with the same name but different typed parameter lists, and the decorator dispatches the correct one at runtime. Pros: clearer code and fewer manual checks. Cons: third-party dependency, and you must define all needed variants—missing ones will raise errors at runtime.
What is the Liskov Substitution Principle (LSP), and how can it be violated?
LSP: Wherever a superclass is expected, a subclass instance should be usable without breaking the program’s logic. Violations occur when a subclass changes expected behavior or exposes operations that violate the base type’s invariants. Example: subclassing list to implement a circular buffer invites use of list methods (indexing, pop) that corrupt the buffer’s invariants. The fix is composition (has-a list internally) instead of inheritance.
How do I choose between is-a (inheritance) and has-a (composition)?
Use is-a when the subtype truly behaves as the supertype and satisfies LSP. Prefer has-a to assemble behaviors at runtime, reduce duplication, and avoid hardcoding. In the toy example, composing independent PlayAction and Sound objects into Toy instances yields:
- Flexibility: swap behaviors at runtime.
- Code sharing: one implementation per behavior.
- Loose coupling and delegation.
- Simpler hierarchies (no multiple inheritance).
How do factory functions/classes help “code to the interface”?
A factory encapsulates object creation, returning instances of a common supertype based on parameters (e.g., which Toy subclass to instantiate and which behaviors to inject). Client code depends on the interface, not concrete classes, enabling runtime selection, easier testing, and less hardcoded construction logic.
What should I watch out for when using Programming by Contract with subclasses?
Overriding methods must not strengthen preconditions or weaken postconditions relative to the superclass. Safe rule: subclass preconditions should be equal to or weaker (a superset of valid inputs), and subclass postconditions should be equal to or stronger (a subset or tighter guarantee). Alternatively, expose boolean pre/post checks in the type’s interface so callers can validate via polymorphism against the actual runtime type.
Software Design for Python Programmers ebook for free