The Novaverse Chronicles₈

A practical look at Python OOP: how class vs instance data works, how __repr__ and __str__ define object behavior, and how super() lets subclasses extend functionality without duplication. Clear patterns every real codebase uses.

The Novaverse Chronicles₈
Classes, voices, and inheritance—the DNA of clean object design.

Chapter Eight — Lesson 03: OOP Basics — The Architecture of Objects

Class variables, object string representations, and super()—the foundation for clean, reusable class design.

Nova: Teslanic, what do you think—should we write a clear instruction about OOP for our readers?

Teslanic: We should. We’ll start with three things that bite everyone: shared class state, how objects speak, and calling the parent without rewriting the world.

Nova: And if they need to back up, they review Error Handling first.

Teslanic: Exactly. Then come back here and build.


1) Class Variables — The Shared DNA

Class variables live on the class, not on each object. Edit at the class level ➜ all instances see the change (unless an instance shadows it).

class Person:
    _species = "Homo sapiens"   # class-level (shared)

    def __init__(self, name, age=0):
        self.name = name        # instance-level (per object)
        self._age = age         # convention: underscore = internal use

    def __repr__(self):
        return f"<Person name={self.name!r} age={self._age}>"

print(Person._species)          # Homo sapiens

p1 = Person("Nova")
p2 = Person("Teslanic")
print(p1._species, p2._species) # Homo sapiens Homo sapiens

# Change at the CLASS level:
Person._species = "Animals"
print(p1._species, p2._species) # Animals Animals

# Shadow at the INSTANCE level:
p1._species = "Cat"
print(p1._species)              # Cat   (instance shadow)
print(p2._species)              # Animals (still class value)

Rule of thumb: Use class variables for true constants/config shared by all (e.g., currency = "USD"). Don’t use them for per-object data like balances or ages.

Inheritance: clean overrides, no confusion

class Creature:
    _species = "Animal"     # default for everyone

    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return f"<{self.__class__.__name__} name={self.name!r} species={self._species}>"

class Dog(Creature):
    _species = "Canis lupus familiaris"  # override

class Cat(Creature):
    _species = "Felis catus"             # override

print(Dog("Rex"))   # <Dog name='Rex' species=Canis lupus familiaris>
print(Cat("Luna"))  # <Cat name='Luna' species=Felis catus>

Gotcha: If you assign obj._species = ... on a single instance, you’re not editing the class variable—you’re creating a new instance attribute that shadows the class one.


2) __repr__ & __str__ — Giving Objects a Voice

By default, printing an object gives you a memory address. Teach your objects to “speak” with __repr__ (developer view) and __str__ (user view).

class Creature:
    _species = "Animal"

    def __init__(self, name):
        self.name = name

    def __repr__(self):
        # precise / dev-friendly
        return f"<Creature name={self.name!r} species={self._species}>"

    def __str__(self):
        # friendly / user-facing
        return f"{self.name} the {self._species}"

c = Creature("Bambam")
print(c)           # Bambam the Animal        (from __str__)
print(repr(c))     # <Creature name='Bambam' species=Animal> (from __repr__)
  • __repr__: unambiguous, used in debugging, shells, logs.
  • __str__: readable summary for end users (print(obj)).
  • If __str__ isn’t defined, Python falls back to __repr__.

Real-world shapes

class User:
    def __init__(self, username, email, active=True):
        self.username = username
        self.email = email
        self.active = active

    def __repr__(self):
        return f"User(username={self.username!r}, email={self.email!r}, active={self.active})"

    def __str__(self):
        return f"{self.username} ({'active' if self.active else 'inactive'})"

u = User("nova", "nova@example.com")
print(u)       # nova (active)
print(repr(u)) # User(username='nova', email='nova@example.com', active=True)

3) super() — Call the Parent, Then Continue

super() doesn’t “do magic.” It simply calls the parent’s version of the method so you can extend behavior without copy-pasting.

class Logger:
    def log(self, message):
        return f"[LOG] {message}"

class SecureLogger(Logger):
    def log(self, message):
        base = super().log(message)   # calls Logger.log
        return f"{base} [ENCRYPTED]"

l = Logger()
sl = SecureLogger()
print(l.log("System started"))   # [LOG] System started
print(sl.log("System started"))  # [LOG] System started [ENCRYPTED]

Mantra: super() = run the parent’s version, then come back here.

Multi-level handoff (grandma → mom → child)

class Grandmother:
    def log(self, message):
        return f"[GRANDMA] {message}"

class Logger(Grandmother):
    def log(self, message):
        base = super().log(message)  # Grandmother.log
        return f"{base} -> [MOTHER] {message}"

class SecureLogger(Logger):
    def log(self, message):
        base = super().log(message)  # Logger.log
        return f"{base} -> [CHILD] {message}"

sl = SecureLogger()
print(sl.log("System started"))
# [GRANDMA] System started -> [MOTHER] System started -> [CHILD] System started

Why this matters: frameworks (Flask/Django/FastAPI) lean on this pattern—parents define defaults, children extend behavior cooperatively.


Nova: So our objects can share what’s truly shared, speak clearly, and respect their parents—without becoming clones.

Teslanic: Exactly. Next, they learn to remember—files and logs.

Checkpoint — What to remember

  • Class variables live on the class. Class-level edits affect everyone. Instance assignment shadows.
  • __repr__ vs __str__: dev vs user. If no __str__, Python uses __repr__.
  • super() calls the parent method so you can extend behavior cleanly.
  • Don’t store per-object data (e.g., balances) in class variables.

Note
If you find any part of this post unclear or technically inaccurate, I would appreciate hearing from you. Improving the precision of these explanations is an ongoing process, and your feedback helps strengthen the material.