Intermediate Python: Practical OOP – The Code and Commands That Really Matter

Intermediate Python OOP: the essentials in one article — real code, diagrams and concrete steps, excerpts from a 36-lesson course.

Intermediate Python: Practical OOP – The Code and Commands That Really Matter

No endless theory here: open the terminal and practice. Here's the essentials of Intermediate Python OOP, extracted directly from a complete 36-lesson course — with real code you can copy-paste right now.

tl;dr
  • Introduction and Installation
  • Python Recap
  • Advanced Functions and Lambdas
  • Classes and Objects
  • Inheritance and Polymorphism
~$ cat ./parcours.md # Python Intermediate OOP — 10 chapters
01
Introduction and Installation
→ Course presentation→ Install Python, VS Code and a virtual environment+ 1 more lessons
02
Python Recap
→ Variables, types and essential data structures→ Conditions, loops and list comprehensions+ 1 more lessons
03
Advanced functions and lambdas
→ Lambda, map, filter, reduce→ *args, **kwargs and default arguments+ 1 more lessons
04
Classes and objects
→ Why OOP? Classes vs instances→ Defining a class: __init__, attributes and methods+ 2 more lessons
05
Inheritance and polymorphism
→ Simple inheritance→ Multiple inheritance and MRO+ 1 more lessons
06
Special dunder methods
→ __str__, __repr__, __len__→ Overloaded operators: __add__, __eq__, __lt__+ 1 more lessons
07
Modules and packages
→ Import, modules and namespaces→ Create your own package with __init__.py+ 1 more lessons
08
Error and exception handling
→ try, except, else, finally→ Custom exceptions+ 1 more lessons
🏁
Final project (+ 2 chapters along the way)
→ You leave with a concrete and demonstrable project

Encapsulation, Properties and Setters

NOTEObjective — Learn how to protect an object's internal attributes and validate their modification using properties, without cluttering the public interface.

Learning Objectives

TIPBy the end of this module — You will know how to use the _x and __x conventions, turn an attribute into a property to add validation, and know when to do so.

What is Encapsulation?

Encapsulation means hiding the internal details of a class and exposing only a controlled interface.

NOTEAnalogy — A TV remote: you press "volume +" without knowing what happens inside the circuits. The interface is simple, the internals are protected.

Python Conventions

Python does not enforce privacy (unlike Java). It relies on conventions based on naming.

ConventionMeaningActual Effect
nomPublicFree access, part of the public API
_nomProtected (convention)"Do not touch unless you know what you're doing"
__nomPrivate (name mangling)Renamed to _ClassName__nom
nom_Avoids keyword collisione.g. class_ instead of class
output
class CompteBancaire:
    def __init__(self, titulaire, solde):
        self.titulaire = titulaire        # public
        self._solde = solde               # convention: do not touch
        self.__pin = "1234"                # private (name mangling)

c = CompteBancaire("Alice", 1000)
print(c.titulaire)                  # OK
print(c._solde)                     # accessible but discouraged
# print(c.__pin)                    # AttributeError !
print(c._CompteBancaire__pin)       # accessible via the mangled name
WARNINGThe __ is not security — It is a mechanism to avoid accidental collisions during inheritance. For real security, use system-level permissions.

The Problem Without Encapsulation

output
class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius

t = Temperature(25)
t.celsius = -500          # absurd!
print(t.celsius)          # -500

No validation. -500 is impossible (absolute zero = -273.15) but Python accepts it.

@property: Turning an Attribute into a Method

A @property makes a call like obj.attribute actually execute a method without changing the usage syntax.

output
class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius

    @property
    def celsius(self):
        return self._celsius

    @celsius.setter
    def celsius(self, valeur):
        if valeur < -273.15:
            raise ValueError("Below absolute zero, impossible")
        self._celsius = valeur

t = Temperature(25)
print(t.celsius)            # 25 (calls the getter)
t.celsius = 30              # calls the setter, OK
t.celsius = -300            # ValueError!
TIPThe superpower — You keep the simple t.celsius syntax but add invisible control. User code does not need to change.

Read-Only Property

For a computed (derived) attribute that should not be modifiable directly.

output
class Cercle:
    def __init__(self, rayon):
        self.rayon = rayon

    @property
    def diametre(self):
        return self.rayon * 2

    @property
    def aire(self):
        from math import pi
        return pi * self.rayon ** 2

c = Cercle(5)
print(c.diametre)         # 10
print(c.aire)             # 78.539...
# c.aire = 100            # AttributeError: no setter

Full Property with Getter, Setter, Deleter

output
class Personne:
    def __init__(self, nom):
        self._nom = nom

    @property
    def nom(self):
        return self._nom

    @nom.setter
    def nom(self, valeur):
        if not isinstance(valeur, str) or len(valeur) < 2:
            raise ValueError("Invalid name")
        self._nom = valeur.title()      # normalization

    @nom.deleter
    def nom(self):
        print("Deleting name")
        self._nom = None

p = Personne("alice")
print(p.nom)              # Alice (normalized)
p.nom = "bob"
print(p.nom)              # Bob
del p.nom                 # Deleting name

Practical Case: CompteBancaire with Validation

output
class CompteBancaire:
    def __init__(self, titulaire, solde=0):
        self.titulaire = titulaire
        self._solde = 0
        self.solde = solde         # goes through the setter!

    @property
    def solde(self):
        return self._solde

    @solde.setter
    def solde(self, valeur):
        if valeur < 0:
            raise ValueError("Negative balance forbidden")
        self._solde = valeur

    def deposer(self, montant):
        if montant <= 0:
            raise ValueError("Amount must be positive")
        self.solde += montant       # goes through the setter

    def retirer(self, montant):
        if montant > self._solde:
            raise ValueError("Insufficient balance")
        self.solde -= montant

c = CompteBancaire("Alice", 1000)
c.deposer(500)
print(c.solde)            # 1500
# c.solde = -100          # ValueError

Simple Inheritance

NOTEObjective — Learn how to create a new class from an existing one, reusing its code and adapting it. This is the key mechanism to avoid code duplication in OOP.

Learning Objectives

TIPBy the end of this module — You will know how to define a child class that inherits from a parent class, override its methods, and call the parent version with super().

The Concept

Inheritance lets you create a specialized class from a general class. The child class receives everything the parent class offers, then adds or modifies behavior.

NOTEAnalogy — A Dog is an Animal. Every dog has animal behaviors (eat, sleep) plus specific behaviors (bark). We say Dog is-a Animal.

First Example

output
class Animal:
    def __init__(self, nom, age):
        self.nom = nom
        self.age = age

    def manger(self):
        print(f"{self.nom} eats")

    def dormir(self):
        print(f"{self.nom} sleeps")


class Chien(Animal):
    def aboyer(self):
        print(f"{self.nom}: Woof!")


rex = Chien("Rex", 5)
rex.manger()              # inherited from Animal
rex.dormir()              # inherited from Animal
rex.aboyer()              # specific to Chien
print(rex.age)            # inherited

Chien does not define __init__: Python automatically uses the one from Animal.

Overriding a Method

The child class can redefine a parent method to adapt its behavior.

output
class Animal:
    def parler(self):
        print("Generic animal sound")


class Chien(Animal):
    def parler(self):
        print("Woof!")


class Chat(Animal):
    def parler(self):
        print("Meow")


for a in [Chien(), Chat(), Animal()]:
    a.parler()
# Woof!
# Meow
# Generic animal sound
TIPPolymorphism — The same call a.parler() produces a different result depending on the actual class of a. This is one of the 4 pillars of OOP.

super(): Calling the Parent Method

When overriding a method, you often want to extend the parent behavior rather than replace it.

output
class Animal:
    def __init__(self, nom, age):
        self.nom = nom
        self.age = age

    def description(self):
        return f"{self.nom}, {self.age} years old"


class Chien(Animal):
    def __init__(self, nom, age, race):
        super().__init__(nom, age)        # calls Animal.__init__
        self.race = race

    def description(self):
        base = super().description()       # calls Animal.description
        return f"{base}, breed {self.race}"


rex = Chien("Rex", 5, "Labrador")
print(rex.description())     # Rex, 5 years old, breed Labrador
WARNINGRule — As soon as a child class has its own __init__, you must call super().__init__(...) to initialize the parent's attributes. Skipping it = guaranteed bugs.

Checking Parentage: isinstance and issubclass

output
rex = Chien("Rex", 5, "Labrador")

print(isinstance(rex, Chien))      # True
print(isinstance(rex, Animal))     # True (inheritance)
print(isinstance(rex, Chat))       # False

print(issubclass(Chien, Animal))   # True
print(issubclass(Animal, Chien))   # False
TIPPythonic — Use isinstance(x, Animal) rather than type(x) == Animal. The first form respects inheritance.

When to Use Inheritance?

RelationshipInheritance?
Dog is-an AnimalYes
Novel is-a BookYes
Car has-a EngineNo — composition
Library contains BooksNo — composition
NOTEGolden rule — Prefer composition over inheritance. If the relationship is not a true specialization, use an attribute instead of inheritance.

Concrete Case: Library Application

output
class Livre:
    def __init__(self, titre, auteur, isbn):
        self.titre = titre
        self.auteur = auteur
        self.isbn = isbn
        self.disponible = True

    def description(self):
        return f"{self.titre} by {self.auteur}"


class Roman(Livre):
    def __init__(self, titre, auteur, isbn, genre):
        super().__init__(titre, auteur, isbn)
        self.genre = genre

    def description(self):
        return f"{super().description()} — {self.genre} novel"


class LivreNumerique(Livre):
    def __init__(self, titre, auteur, isbn, format_fichier):
        super().__init__(titre, auteur, isbn)
        self.format_fichier = format_fichier

    def telecharger(self):
        print(f"Downloading {self.titre}.{self.format_fichier}")


dune = Roman("Dune", "Herbert", "978...", "science-fiction")
ebook = LivreNumerique("1984", "Orwell", "978...", "epub")

print(dune.description())       # Dune by Herbert — science-fiction novel
print(ebook.description())       # 1984 by Orwell
ebook.telecharger()              # Downloading 1984.epub

Common Hierarchies in Python

Lambda, map, filter, reduce

NOTEObjective — Discover anonymous functions (lambda) and higher-order functions (map, filter, reduce), which let you transform and filter data very concisely.

Learning Objectives

TIPBy the end of this module — You will know when to use a lambda and how to choose between map/filter and a list comprehension depending on context.

What is a lambda?

A lambda is an anonymous function (no name) that fits on a single line. It is used for short operations passed as arguments to another function.

Syntax

output
lambda parameters: expression

Comparison

output
def doubler(x):
    return x * 2

doubler_lambda = lambda x: x * 2

print(doubler(5))         # 10
print(doubler_lambda(5))  # 10
NOTEAnalogy — A regular def is like writing a named recipe in a book. A lambda is scribbling a recipe on a sticky note you hand over immediately.

Multi-parameter Lambdas

output
somme = lambda a, b: a + b
print(somme(3, 4))                       # 7

est_pair = lambda n: n % 2 == 0
print(est_pair(8))                       # True
WARNINGLimit — A lambda contains only one expression. No explicit return, no multiple lines. As soon as things get more complex, use def.

map(): Transforming Each Element

map(function, iterable) applies the function to every element and returns an iterator. It is often wrapped in list().

output
prix = [10, 25, 7, 50]
prix_ttc = list(map(lambda p: p * 1.20, prix))
print(prix_ttc)        # [12.0, 30.0, 8.4, 60.0]

mots = ["python", "poo", "code"]
majuscules = list(map(str.upper, mots))
print(majuscules)      # ['PYTHON', 'POO', 'CODE']

With Multiple Sequences

output
a = [1, 2, 3]
b = [10, 20, 30]
sommes = list(map(lambda x, y: x + y, a, b))
print(sommes)          # [11, 22, 33]

filter(): Keeping What Passes the Test

output
notes = [12, 8, 15, 6, 18, 11]
reussies = list(filter(lambda n: n >= 10, notes))
print(reussies)        # [12, 15, 18, 11]

mots = ["chat", "chien", "ours", "lion"]
courts = list(filter(lambda m: len(m) <= 4, mots))
print(courts)          # ['chat', 'ours', 'lion']
go-further

This article covers the most useful excerpts — the complete Intermediate Python OOP course (11 chapters, 36 lessons, corrected exercises and final project) takes you all the way.

./access-the-complete-course free course: Mastering Claude Code

FAQ

How long does it take to learn Intermediate Python OOP?
With a structured progression (11 chapters, 36 short practical lessons), you reach an operational level in a few weeks at 30–60 minutes per day. The key is to practice each concept immediately.
Are there any prerequisites?
Basic computer literacy is enough. If you can use a terminal and read simple code, you are ready.
Where to start concretely?
Reproduce the commands from this article, then follow the complete Intermediate Python OOP course: it chains the 36 lessons in order, with exercises and a final project.

📬 Want to receive this kind of guide every week? Subscribe for free — real code, zero fluff.