Lecture 7 · Part 2
Polymorphism, encapsulation and designing fault-tolerant banking systems
Today we will look at how to design flexible and secure architectures. With real-world examples from fintech - how to manage complex data flows, keep passwords out of logs, and orchestrate dozens of payment gateways through a single interface.
Lecture 6 Recap
In the previous session we laid the foundation: we learned to package state and behavior into classes, and we got acquainted with inheritance.
Before moving on to more advanced concepts - dynamic polymorphism and encapsulation - we need to lock in the terminology on basic examples. Without that foundation, it is easy to get lost in the mechanics of MRO and Name Mangling.
Recap
Let us compare two approaches to managing data, using bank account bookkeeping as an example.
A class is a user-defined data type that binds financial state and the logic for working with it into a single structure.
Recap
A programmatic description of structure. On its own it stores no specific client's data and takes up no memory for data.
A concrete object in memory - for example, a client's account with a real balance. Millions of isolated objects can be created from a single class.
Recap
Variables inside an object that reflect its current state: account_number, currency, is_blocked.
Functions inside a class with access to the object's state: withdraw(), block_account().
💡 A well-designed class manages its own attributes independently, and external code interacts with it only through method calls.
Recap
__init__ method and the self argument__init__ is the object's constructor, called automatically after memory is allocated. self is an explicit reference to the current instance.
Context
self vs Java thisself is written as the first argument of every method. "Explicit is better than implicit."
The JVM passes the reference automatically; inside the method it is available through this.
Recap
Recap
__str__ methodRecap
Recap
super()Recap
When working with Python, you use OOP every minute - almost every call through a dot is a method of an object.
Recap
| Concept | In brief | Meaning in architecture |
|---|---|---|
| Class / Instance | Description of structure vs object in RAM | Product template and a real client account |
| Attribute / Method | State variable vs function | Balance (data) and the transfer method (behavior) |
| __init__ / self | Constructor and reference to the context | Entry point at creation and a reference to the instance |
| Inheritance / super() | Extension without duplication | CreditAccount built on BasicAccount |
Course map
| Concept | Status | Stage |
|---|---|---|
| Inheritance | 🟢 DONE | Syntax of class extension and super(). |
| Polymorphism | 🟡 CURRENT TOPIC | A single interface for different payment gateways. |
| Encapsulation | ⚪ UPCOMING | Hiding critical data and validation. |
| Abstraction | ⚪ UPCOMING | Separating business logic from implementation details. |
Today
_ and __ modifiers, @property, setters@staticmethod, @classmethod+, ==, len() operators for user-defined objectsPolymorphism
Picture a payment terminal at a checkout. It does not care what exactly the customer presents: a UzCard plastic card, a smartphone with Humo Pay, or a QR code from Milly Bank. For the terminal there is one interface - "accept the payment." And how the charge actually happens depends on the specific payment instrument.
Polymorphism is the ability of a program to interact with different objects through a single interface, relying only on the method name and not on the object's internal structure.
Polymorphism
Through inheritance and overriding. A parent class is created; descendants implement a method with the same name but their own logic.
Duck typing. No inheritance is required: if independent classes have a method with the same name, it can be called polymorphically.
Polymorphism
Classical polymorphism
Overriding is when a descendant class declares a method with the same name as the parent's but implements it in its own way.
When obj.greet() is called, Python searches for the method starting from the object's own class. The descendant's version "overrides" the parent's. And super().greet() lets you additionally run the parent's version as well.
Classical polymorphism
The base class holds the shared logic. The descendant class overrides the method:
super() calls the parent's version. UzCardGateway adds its own behavior without losing the shared core logic.
Classical polymorphism
Humo and Milly Bank override the method the same way. run_checkout works with any gateway:
Method overriding
Each gateway overrides process_payment in its own way. super() first runs the shared core logic (writing to the log), then the code of the specific gateway. The run_checkout function calls a single method and does not know which class was passed to it.
Reinforcing overriding
Three tariffs override calculate_fee with their own formula. The loop calls the same method - Python itself substitutes the right implementation based on the object's type.
In other languages
| Criterion | Python | Java |
|---|---|---|
| Default behavior | All methods are virtual. A matching name in a descendant overrides the parent method. | Requires matching signatures. The @Override annotation is recommended. |
| Type checking | None at the runtime level. | Enforced by the compiler. |
| Lookup principle | Dynamic lookup along the MRO at execution time. | Via the virtual method table (VMT) at compile time. |
Risk of overriding
The cause: format_inn returned None, and the call None.upper() failed - the NoneType type has no such method.
Risk of overriding
In compiled languages such a typo would cause a build error. Python silently creates a new method and calls the parent's one.
Type hierarchy
objectobject is a built-in class that Python itself created as the foundation of the entire type system. It exists before your code: it need not be defined or imported.
Any class in Python is, ultimately, a descendant of object. It is precisely from object that all objects get their basic capabilities: __init__, __str__, __eq__, __hash__ and others.
Type hierarchy
object automaticallyWhen you write a class without specifying a parent, Python silently adds object as the base class. The two listings below are completely identical:
The parentheses with object can be omitted - Python will supply it itself. That is why we "do not see" the parent in the code, but it is always there.
Type hierarchy
objectIf the parent class is not specified explicitly, Python inherits from object - the top of the entire hierarchy.
In other languages
object vs Java java.lang.Object| Parameter | Python | Java |
|---|---|---|
| Auto-linking | Closes off any user-defined hierarchy. | The absolute root of the object model. |
| Base set | __new__, __init__, __repr__, __eq__. | toString(), equals(), hashCode(). |
| Multiple inheritance | The final point of the MRO. | Not applicable - Java supports single inheritance only. |
Hierarchy conflict
Deep dive
Python uses C3 linearization: when a method is called, classes are scanned in MRO order, and the first one found is executed.
Cooperative inheritance
super() and the MROsuper() calls a method not on the "parent from the declaration," but on the next class in the current object's MRO chain.
super() inside Left passed control to Right, even though they are not directly related - this is cooperative multiple inheritance.
Hierarchy conflict
Polymorphism
Polymorphism
A single interface lets you process sets of heterogeneous objects in ordinary loops - without type checks via if isinstance().
Architecture
From an architectural standpoint, polymorphism isolates business logic from the technical details of specific vendors.
When connecting a new gateway (Click or Payme, for example), there is no need to change the core code. It is enough to create a class that descends from PaymentGateway and implement process_payment. The rest will work automatically.
Takeaways
| Rule | Why it matters |
|---|---|
| Keep the method name exact | A typo will create a new method, while the old one keeps running silently |
| Keep the return type | The parent code expects to receive a specific data type |
| Call super() when needed | If the base logic (audit log) must be executed |
| Keep the argument signature | Otherwise the polymorphic call will receive unexpected data |
Dynamic polymorphism
"If an object behaves like a duck - it is a duck." Common ancestors are not required: if there is a method with the right name, it can be called.
Caution
OOP in real projects
Scikit-learn is built entirely on duck typing: any model with the .fit() and .predict() methods works in a shared pipeline.
Takeaways
| Characteristic | Overriding | Duck typing |
|---|---|---|
| Requires a common ancestor | Yes | No |
| Contract enforcement | Through the hierarchy | Only by the method name |
| When errors become visible | At the design stage | Only at runtime |
| Where it is used | Gateways of one system | Independent libraries |
Course map
| Concept | Status | Description |
|---|---|---|
| Inheritance | 🟢 DONE | Syntax of class extension and super(). |
| Polymorphism | 🟢 DONE | Overriding, MRO and duck typing. |
| Encapsulation | 🟡 CURRENT TOPIC | Hiding critical fields from external access. |
| Abstraction | ⚪ UPCOMING | Separating business logic from implementation details. |
Encapsulation
A plain-text password, readable and writable by anyone - a straight road to a leak.
Encapsulation
Encapsulation solves two tasks:
🔐 You cannot change the balance or the password directly. For that, the methods change_password() (with a check of the old password and hashing) and deposit() (with amount validation) are provided.
In other languages
The private modifier blocks access at the compilation level. Accessing user.password from the outside is a compiler error.
There are no physical restrictions. Protection rests on attribute naming (_ and __). Developers follow it by agreement.
Encapsulation
_A single underscore is a signal: "this field is internal, not intended for use outside the class."
from module import *Encapsulation
__ and Name ManglingA double underscore turns on Name Mangling: __password_hash becomes _ClassName__password_hash. This protects the field from being accidentally overridden during inheritance.
Encapsulation
@property decorator - gettersIf you need to read data but control the process or mask the output - @property is used. It turns a method into a "virtual attribute" - accessed without parentheses.
Why @property
Why @property
Why @property
If an attribute was public and then logic on read became necessary - @property lets you add it without changing the call syntax. All external code keeps working.
That is why in Python you start with an ordinary attribute and turn it into a @property only when logic appears.
In other languages
Every field is wrapped from day one into getBalance() / setBalance(), even with no logic inside. A large amount of boilerplate code.
We start with an ordinary attribute. When necessary, we turn it into a @property - and all external code keeps working without changes.
Encapsulation
@property.setter - validation on writeA setter intercepts assignment via = and checks the data before storing it.
Under the hood
@property + setter pairing worksThe getter and setter names must match - that is why the decorator is written as @limit.setter, and not simply @setter.
Encapsulation
Encapsulation
Constructor, a protected balance, and a deposit with a check:
The balance is closed to direct writes: it is read through @property and changed only through methods.
Encapsulation
The same class - a withdrawal method with a double check:
The method itself checks the amount and the remaining balance - an incorrect operation simply will not go through.
Encapsulation
Encapsulation
| Tool | What it does | When to use |
|---|---|---|
| _name | A signal of internal ownership | Fields not intended for external use |
| __name | Name Mangling - renames the field in memory | Critical data: passwords, tokens, PIN codes |
| @property | Controlled reading | Logic on read, masking, computed values |
| @name.setter | Validation before writing | Checking data before storing it |
| @property without a setter | Read-only | Immutable identifiers: contract ID, account number |
Takeaways
| Concept | Status | What problem it solves | Key tools |
|---|---|---|---|
| Inheritance | 🟢 | Code duplication across similar classes | class Child(Parent), super() |
| Polymorphism | 🟢 | Code's dependence on specific implementations | Overriding, Duck Typing, MRO |
| Encapsulation | 🟢 | Direct access to data breaks the logic | _, __, @property, setter |
| Abstraction | ⚪ | High-level code is mixed with the details | ABC, @abstractmethod |
Class level
An instance attribute is created via self.field separately for each object. A class attribute is declared in the class body - one copy for all instances.
Class level
When changed through an instance (contract1.base_interest_rate = 0.25), Python will create a new attribute on that instance - the class attribute will remain unchanged.
Practice
Two Jupyter notebooks for Part 1 of the lecture.
Open in Jupyter Notebook, JupyterLab or VS Code.