A progressive learning project demonstrating Object-Oriented Programming principles in Python using a single domain: a Library Catalog System.
Each module introduces one OOP concept, builds on the previous one, and is covered by a matching test file using pytest.
Python is an interpreted language. There is no separate compile step before you can run code. When you execute a file, the Python interpreter reads it line by line and runs it immediately.
Your .py file → Python interpreter → Output / errors
Compare this to compiled languages:
| Language | Flow |
|---|---|
| C / C++ / Go | write → compile → binary → run |
| Java / C# | write → compile → bytecode → run on VM |
| Python | write → run directly |
This means:
- Syntax errors appear the moment you run the file (or when pytest tries to collect it).
- Type errors only appear at runtime unless you run a separate type checker like
mypy. - The feedback loop is fast — save, run, see result immediately.
Even though there is no manual compile step, Python does compile your .py files to
bytecode (.pyc) behind the scenes. This is an optimisation — it avoids re-parsing
unchanged files on the next run. You will see these appear as __pycache__ folders
automatically. See the __pycache__ section below.
python/
├── pyproject.toml # Project metadata + pytest configuration
├── requirements.txt # Dev dependencies (pytest, pytest-cov)
│
├── src/
│ └── oop_demo/
│ ├── __init__.py # Marks oop_demo as a package
│ ├── basics/
│ │ ├── __init__.py # Marks basics/ as a sub-package
│ │ ├── catalog_item.py
│ │ └── README.md
│ ├── inheritance/
│ │ ├── __init__.py
│ │ ├── media.py
│ │ └── README.md
│ ├── encapsulation/
│ │ ├── __init__.py
│ │ ├── member.py
│ │ └── README.md
│ ├── abstraction/
│ │ ├── __init__.py
│ │ ├── interfaces.py
│ │ ├── borrowable_item.py
│ │ └── README.md
│ ├── polymorphism/
│ │ ├── __init__.py
│ │ ├── rich_item.py
│ │ └── README.md
│ └── patterns/
│ ├── __init__.py
│ ├── factory.py
│ ├── observer.py
│ ├── repository.py
│ └── README.md
│
└── tests/
├── __init__.py
├── conftest.py # Shared pytest fixtures
├── test_01_basics.py
├── test_02_inheritance.py
├── test_03_encapsulation.py
├── test_04_abstraction.py
├── test_05_polymorphism.py
└── test_06_patterns.py
A folder containing an __init__.py file is called a package in Python.
Without it, Python will not treat the folder as importable — it is just a directory.
src/oop_demo/basics/catalog_item.py
For this import to work anywhere in the project:
from oop_demo.basics.catalog_item import CatalogItemPython needs to find __init__.py at each level of the path:
src/oop_demo/__init__.py ← makes oop_demo a package
src/oop_demo/basics/__init__.py ← makes basics a sub-package
In this project every __init__.py is empty. That is the modern convention:
let each module be imported by its full path. The file just needs to exist.
You can put code in __init__.py — for example, to create a shortcut import:
# src/oop_demo/__init__.py (if we wanted shortcuts)
from oop_demo.basics.catalog_item import CatalogItem
from oop_demo.inheritance.media import BookThat would allow from oop_demo import CatalogItem instead of the full path.
We keep them empty here to make the package structure explicit and easy to navigate.
tests/__init__.py tells pytest (and Python) that the test folder is also a package.
This avoids name collision issues when pytest collects test files across nested folders.
When Python first imports a module, it compiles the .py source to bytecode and
saves it as a .pyc file inside a __pycache__ folder next to the source file:
src/oop_demo/basics/
├── __init__.py
├── catalog_item.py
├── README.md
└── __pycache__/
├── __init__.cpython-314.pyc
└── catalog_item.cpython-314.pyc
The filename encodes the Python version (cpython-314 = CPython 3.14) so multiple
Python versions can coexist without overwriting each other's cache.
Bytecode is a lower-level representation of your code that the Python Virtual Machine
(PVM) executes directly — similar to Java's .class files. It is not machine code
and is not platform-specific. It cannot be run without Python installed.
| Without cache | With cache |
|---|---|
Parse + compile .py on every run |
Parse + compile once, load .pyc on subsequent runs |
| Slower startup for large projects | Faster import times |
Python checks the modification timestamp of the .py file on each import.
If the source changed, it recompiles automatically. You never need to manage this manually.
__pycache__is regenerated automatically on the next run — you can delete it freely.- It should be listed in
.gitignoreso it is never committed to version control. - You can tell Python to skip writing it entirely:
python -B your_script.py
Python gives you several tools at different levels of strictness.
python src/oop_demo/basics/catalog_item.pyIf there is a SyntaxError, Python reports it immediately with the file and line number.
This is the equivalent of a compile check.
python -m py_compile src/oop_demo/basics/catalog_item.pyParses and compiles the file but does not run it. No output means no errors. Useful when you want to check syntax without side effects.
# run everything from the project root
python -m pytest
# run one module's tests
python -m pytest tests/test_02_inheritance.py -v
# stop at the first failure
python -m pytest -x
# show full output even on passing tests
python -m pytest -sIf any file has a syntax error, pytest reports it during the collection phase before a single test runs. A passing test suite guarantees both correct syntax and correct behaviour.
Python's type hints (: str, -> bool, etc.) are not enforced at runtime.
mypy reads them statically and reports type mismatches without running anything:
python -m pip install mypy
python -m mypy src/Example output when something is wrong:
src/oop_demo/encapsulation/member.py:42: error: Argument 1 to "add_fine" has
incompatible type "str"; expected "float" [arg-type]
This is the closest Python gets to a compiled-language type checker.
python -m pytest --cov=oop_demo --cov-report=term-missingAdds a Miss column to the test output showing exactly which line numbers have
no test exercising them.
All modules use a Library Catalog as the domain:
| Class | Module | Role |
|---|---|---|
CatalogItem |
basics | Base entity — title, author, year, ISBN |
Book, Magazine, AudioBook, DVD |
inheritance | Specialised media types |
Member |
encapsulation | Library member with fines and borrow limits |
Borrowable, Searchable |
abstraction | Contracts items must fulfil |
BorrowableBook |
abstraction | Combines ABC + concrete class |
RichCatalogItem |
polymorphism | Full Python data model (dunder methods) |
MediaFactory |
patterns | Centralised object creation |
LibraryEventBus |
patterns | Observer pattern for borrow events |
CatalogRepository |
patterns | Storage abstraction + in-memory impl |
# Install dependencies
python -m pip install -r requirements.txt
# Run all tests
python -m pytest
# Run with coverage report
python -m pytest --cov=oop_demo --cov-report=term-missing
# Run a single module's tests
python -m pytest tests/test_03_encapsulation.py -v
# Syntax check a single file without running it
python -m py_compile src/oop_demo/basics/catalog_item.py
# Type check the whole source tree
python -m mypy src/Follow the modules in order — each folder has its own README explaining the concept, the Python feature that implements it, and what the tests verify.
| # | Folder | Concept | Key Python feature |
|---|---|---|---|
| 01 | basics/ |
Classes & state | __init__, class vars, @classmethod |
| 02 | inheritance/ |
Reuse & extension | super(), MRO, method overriding |
| 03 | encapsulation/ |
Information hiding | __private, _protected, @property |
| 04 | abstraction/ |
Contracts | ABC, @abstractmethod, Protocol |
| 05 | polymorphism/ |
Rich behaviour | Dunder methods, @total_ordering |
| 06 | patterns/ |
Design patterns | Factory, Observer, Repository |
| Tool | Type | Purpose |
|---|---|---|
pytest |
third-party | Test runner — fixtures, assertions, collection |
pytest-cov |
third-party | Coverage reporting |
mypy |
third-party (optional) | Static type checker |
abc |
stdlib | Abstract Base Classes and @abstractmethod |
typing |
stdlib | Protocol, runtime_checkable, type hints |
dataclasses |
stdlib | Boilerplate-free value objects with @dataclass |
functools |
stdlib | total_ordering — derive comparison operators |
py_compile |
stdlib | Syntax-only validation without execution |