Syntax Cache
BlogMethodFeaturesHow It WorksBuild a Game
  1. Home
  2. Python
  3. Python Classes Practice: self, super(), __repr__ vs __str__, inheritance
Python31 exercises

Python Classes Practice: self, super(), __repr__ vs __str__, inheritance

Practice Python class syntax: the self parameter, super().__init__() in subclasses, __repr__ vs __str__ for debugging, and the mutable class attribute trap.

Common ErrorsQuick ReferencePractice
Warm-up1 / 2

Can you write this from memory?

Write the class header for a class named `Item` (header only, no body)

On this page
  1. 1The self parameter: what it actually is
  2. The classic error
  3. 2Class vs instance attributes: the mutable trap
  4. 3Inheritance and super().__init__()
  5. Why super() instead of Parent.__init__()?
  6. 4Composition vs inheritance
  7. 5__repr__ vs __str__: the debugging rule
  8. In the REPL
  9. 6Dunder methods: making objects Pythonic
  10. Example: equality comparison
  11. 7Properties: validated attributes
  12. 8When to use @dataclass
  13. 9Quick reference
  14. 10References
The self parameter: what it actually isClass vs instance attributes: the mutable trapInheritance and super().__init__()Composition vs inheritance__repr__ vs __str__: the debugging ruleDunder methods: making objects PythonicProperties: validated attributesWhen to use @dataclassQuick referenceReferences

You can write Python's class Dog: from memory. But do you blank on super().__init__() syntax? Can you explain why __repr__ matters more than __str__ for debugging? Do you know the difference between defining items = [] at class level vs inside __init__?

These are the Python Object Oriented Programming details that cause TypeError: missing 1 required positional argument: 'self' at 2 AM.

The basics:

  • self is the instance—always the first parameter in methods
  • super().__init__(...) calls the parent's __init__ with those args
  • __repr__ is for developers (debugging), __str__ is for users (display)
  • Class attributes are shared; instance attributes (self.x) are per-instance
Related Python Topics
Python Function Arguments: defaults, *args, keyword-only, **kwargsPython Decorators: wraps, factories, stacking order & lru_cachePython Exception Handling: try/except/else/finally, raise from, custom exceptions

fido.bark() becomes Dog.bark(fido). Methods need self because it's how they access the instance—always include it as the first parameter.

Every instance method receives the instance as its first argument. By convention, we call it self:

class Dog:
    def bark(self):
        return f"{self.name} barks!"

fido = Dog()
fido.name = "Fido"
fido.bark()  # Python calls Dog.bark(fido)

When you call fido.bark(), Python translates it to Dog.bark(fido). That's why methods need self—it's how they access the instance.

The classic error

If you forget self in the signature or call a method on the class instead of an instance:

# Missing self in definition
class Bad:
    def greet():  # Bug: no self
        return "Hello"

Bad().greet()  # TypeError: greet() takes 0 positional arguments but 1 was given

# Calling on class instead of instance
class Dog:
    def bark(self):
        return "Woof!"

Dog.bark()  # TypeError: missing 1 required positional argument: 'self'

Fix: Always include self as the first parameter, and call methods on instances, not classes.


Ready to practice?

Start practicing Python Classes: self, super(), __repr__ vs __str__, inheritance with spaced repetition

items = [] at class level creates ONE shared list. Use self.items = [] in __init__ so each instance gets its own. Class attributes are for shared constants only.

This is one of the most common Python OOP bugs:

class Team:
    members = []  # Class attribute—shared!

    def add(self, name):
        self.members.append(name)

alpha = Team()
beta = Team()
alpha.add("Alice")
print(beta.members)  # ['Alice'] — wait, what?

Why it happens: members = [] creates ONE list at class definition time. All instances share that same list object.

The fix: Initialize mutable attributes in __init__:

class Team:
    def __init__(self):
        self.members = []  # Instance attribute—each instance gets its own

alpha = Team()
beta = Team()
alpha.add("Alice")
print(beta.members)  # [] — correct!

Rule of thumb:

  • Class attributes for shared constants: species = "dog"
  • Instance attributes for per-instance state: self.name = name

Python attribute lookup chain: data descriptors first, then instance.dict, then class and parents via MRO, then getattr fallback, and finally AttributeError


If your subclass has __init__, you must call super().__init__(...) or the parent's attributes won't exist. Use super() instead of calling the parent class directly.

When you override __init__ in a subclass, the parent's __init__ doesn't run automatically:

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

class Dog(Animal):
    def __init__(self, name, breed):
        # If we forget to call super().__init__(), self.name won't exist!
        self.breed = breed

fido = Dog("Fido", "Lab")
print(fido.name)  # AttributeError: 'Dog' object has no attribute 'name'

Always call the parent's __init__:

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)  # Runs Animal.__init__
        self.breed = breed

fido = Dog("Fido", "Lab")
print(fido.name)  # "Fido" — works!

Why super() instead of Parent.init()?

super() follows the Method Resolution Order (MRO), which matters for multiple inheritance. Direct parent calls can break in diamond inheritance patterns.

Diamond inheritance and MRO: class D inherits from B and C which both inherit from A. Python linearizes the lookup order as D, B, C, A, object


Inheritance (is-a): Use when the subclass truly IS a specialized version of the parent.

class Animal:
    def eat(self):
        print("Eating...")

class Dog(Animal):  # A Dog IS an Animal
    def bark(self):
        print("Woof!")

Composition (has-a): Use when one class USES another but isn't a subtype.

class Engine:
    def start(self):
        print("Vroom!")

class Car:
    def __init__(self):
        self.engine = Engine()  # A Car HAS an Engine

    def start(self):
        self.engine.start()

Inheritance vs composition: Dog inherits from Animal (is-a) with a solid arrow; Car contains an Engine (has-a) with a diamond at the Car side

Prefer composition when you're unsure. It's more flexible:

  • You can swap implementations (dependency injection) — this is the core idea behind the Strategy pattern
  • You avoid inheriting methods you don't want
  • You can compose multiple behaviors without multiple inheritance complexity

Both methods convert objects to strings, but they serve different audiences:

MethodPurposeCalled by
__repr__Developer representation (unambiguous)repr(), REPL, debuggers
__str__User representation (readable)str(), print()

If you only implement one, implement __repr__. Python uses it as a fallback when __str__ is missing.

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        # Unambiguous: could be used to recreate the object
        return f"Point({self.x!r}, {self.y!r})"

    def __str__(self):
        # Readable: for end users
        return f"({self.x}, {self.y})"

p = Point(3, 4)
print(repr(p))  # Point(3, 4)  — for devs
print(str(p))   # (3, 4)       — for users
print(p)        # (3, 4)       — print() uses __str__

In the REPL

>>> p = Point(3, 4)
>>> p           # Uses __repr__
Point(3, 4)
>>> print(p)    # Uses __str__
(3, 4)

The !r format spec: f"{self.x!r}" calls repr() on self.x, ensuring strings show with quotes.


Dunder (double underscore) methods let your objects work with Python syntax:

MethodEnables
__eq__(self, other)== comparison
__lt__, __le__, __gt__, __ge__<, <=, >, >=
__hash__(self)Use as dict key or set member
__len__(self)len(obj)
__iter__(self)for x in obj
__getitem__(self, key)obj[key]
__add__(self, other)obj + other
__bool__(self)Truthiness in if obj:

Example: equality comparison

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __eq__(self, other):
        if not isinstance(other, Point):
            return NotImplemented  # Let Python try other.__eq__
        return self.x == other.x and self.y == other.y

Point(1, 2) == Point(1, 2)  # True (compares values)

Without __eq__, == compares identity (same object in memory), not value. When your objects need to notify others about state changes, the Observer pattern provides a clean structure for event-driven communication.


Use @property to add getters/setters with validation:

class Circle:
    def __init__(self, radius):
        self.radius = radius  # Uses the setter

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        if value <= 0:
            raise ValueError("Radius must be positive")
        self._radius = value

c = Circle(5)
c.radius = -1  # ValueError: Radius must be positive

Note: The underlying data is stored in _radius (with underscore). The radius property provides the public interface. See the decorators page for more on how @property works.


If your class is primarily a data container, @dataclass eliminates boilerplate:

# Without dataclass: manual __init__, __repr__, __eq__
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Point(x={self.x}, y={self.y})"

    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

# With dataclass: all generated automatically
from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

Use @dataclass when:

  • You just need to store data with some methods
  • Default __init__, __repr__, __eq__ are sufficient

Use a regular class when:

  • You need complex initialization logic
  • You want full control over attribute storage
  • You're modeling behavior-heavy objects, not data — the Factory pattern is useful when object creation itself is complex

For a printable reference of class syntax, see the Python OOP cheat sheet.


# Method signature (always include self)
def method(self, arg1, arg2):

# Call parent __init__
super().__init__(arg1, arg2)

# Instance attribute (in __init__)
self.name = name

# Class attribute (shared—careful with mutables!)
class Foo:
    class_attr = "shared"

# __repr__ for developers
def __repr__(self):
    return f"ClassName({self.attr!r})"

# __str__ for users
def __str__(self):
    return f"Human-readable: {self.attr}"

# Property with validation
@property
def x(self):
    return self._x

@x.setter
def x(self, value):
    if not valid(value):
        raise ValueError(...)
    self._x = value

  • Python Tutorial: Classes
  • Python Data Model: Special Method Names
  • Real Python: repr vs str
  • PEP 557 – Data Classes
  • Python Tutorial: Inheritance

When to Use Python Classes: self, super(), __repr__ vs __str__, inheritance

  • Model entities with state (attributes) and behavior (methods) that belong together.
  • Enforce invariants—validation logic lives inside the class, not scattered everywhere.
  • Share behavior across related types with inheritance (is-a) or composition (has-a).
  • Hide implementation details behind a clean API using properties or private attributes.
  • Use `@dataclass` when you just need a data container with auto-generated methods.

Check Your Understanding: Python Classes: self, super(), __repr__ vs __str__, inheritance

Prompt

Design a class that validates age on assignment and exposes it as a property.

What a strong answer looks like

Store in a private attribute (`self._age`), validate in `__init__` and in the `@property` setter. Raise `ValueError` for invalid ages. Bonus: mention that `@property` is powered by descriptors.

What You'll Practice: Python Classes: self, super(), __repr__ vs __str__, inheritance

Class definitions and __init__Instance attributes (self.x) vs class attributesMethods: instance, @classmethod, @staticmethodInheritance with super().__init__()The self parameter and why methods need it__repr__ vs __str__ (developers vs users)Other dunder methods (__eq__, __len__, __iter__, __hash__)Properties with @property (basics—see decorators for depth)Composition vs inheritance ("has-a" vs "is-a")Private attributes (_private and __mangled)

Common Python Classes: self, super(), __repr__ vs __str__, inheritance Pitfalls

  • Forgetting self in method definitions—causes "missing positional argument: self"
  • Mutable class attributes (items = []) shared across all instances—use self.items = [] in __init__
  • Not calling super().__init__() in subclasses—parent attributes never get set
  • Defining __str__ but not __repr__—debuggers show the default <Class at 0x...>
  • Calling methods on the class instead of an instance—MyClass.method() has no self
  • Overriding __init__ without calling super() in diamond inheritance—MRO breaks
  • Using == without defining __eq__—compares identity, not value

Python Classes: self, super(), __repr__ vs __str__, inheritance FAQ

What's the difference between __repr__ and __str__?

`__repr__` is for developers—it should be unambiguous and ideally valid Python that could recreate the object. `__str__` is for users—readable but less precise. If only one is defined, use `__repr__`: Python falls back to it when `__str__` is missing.

When do I need to call super().__init__()?

When your subclass has its own `__init__` and you want to run the parent's initialization too. If you forget, the parent's `__init__` never runs—attributes it sets won't exist. Always call it with the arguments the parent expects.

Why am I getting "missing 1 required positional argument: self"?

You're calling a method on the class instead of an instance. `MyClass.method()` doesn't work because there's no `self`. Fix: instantiate first—`obj = MyClass(); obj.method()`—or use `@staticmethod` if you don't need `self`.

Why is my list shared between all instances?

You defined it as a class attribute (`items = []` in the class body). Class attributes are shared across all instances. Move it inside `__init__` as `self.items = []` to make each instance have its own list.

When is a class better than a dict?

Use classes when you need behavior (methods), validation (properties), or invariants that should always accompany the data. Use dicts for unstructured or dynamic data where keys aren't known ahead of time.

What are dunder methods used for?

They let your objects integrate with Python syntax: `__str__` for `str()` and `print()`, `__repr__` for the REPL, `__eq__` for `==`, `__len__` for `len()`, `__iter__` for `for` loops, etc.

When should I use @dataclass instead of writing __init__ myself?

When your class is primarily a data container. `@dataclass` auto-generates `__init__`, `__repr__`, and `__eq__`. Use it to reduce boilerplate; switch to a regular class if you need complex initialization logic or want full control.

Should I use inheritance or composition?

Use inheritance for "is-a" relationships (a Dog *is* an Animal). Use composition for "has-a" relationships (a Car *has* an Engine). Composition is often more flexible—prefer it unless inheritance clearly fits.

Why does __repr__ matter more than __str__ for debugging?

The REPL and debuggers display objects using `__repr__`. If you only implement one, implement `__repr__`—it's the fallback for `str()` too. Include type and key state so you can tell objects apart in logs.

What does self actually refer to?

`self` is the instance the method was called on. When you call `dog.bark()`, Python passes `dog` as `self`. It's just a convention—you could name it anything—but always name it `self`.

Python Classes: self, super(), __repr__ vs __str__, inheritance Syntax Quick Reference

Basic class with __init__
class Dog:
    def __init__(self, name):
        self.name = name  # Instance attribute

    def bark(self):
        return f"{self.name} barks!"
Instance vs class attribute
# Class attribute (shared—dangerous with mutables)
class Bad:
    items = []  # Shared across ALL instances!

# Instance attribute (per-instance—safe)
class Good:
    def __init__(self):
        self.items = []  # Each instance gets its own list
Inheritance with super()
class Animal:
    def __init__(self, name):
        self.name = name

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)  # Call parent __init__
        self.breed = breed
__repr__ vs __str__
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Point({self.x!r}, {self.y!r})"  # For devs

    def __str__(self):
        return f"({self.x}, {self.y})"  # For users
Common TypeError fix
# Wrong: calling method on class
Dog.bark()  # TypeError: missing argument "self"

# Right: call on instance
fido = Dog("Fido")
fido.bark()  # Works!
Property for validation
class Person:
    def __init__(self, age):
        self.age = age  # Uses the setter

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, value):
        if value < 0:
            raise ValueError("Age cannot be negative")
        self._age = value
Equality with __eq__
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __eq__(self, other):
        if not isinstance(other, Point):
            return NotImplemented
        return self.x == other.x and self.y == other.y
Dataclass alternative
from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

# Auto-generates __init__, __repr__, __eq__
p = Point(1.0, 2.0)
print(p)  # Point(x=1.0, y=2.0)

Python Classes: self, super(), __repr__ vs __str__, inheritance Sample Exercises

Example 1Difficulty: 2/5

Write the method header for an instance method named `add` (header only, no body)

def add(self):
Example 2Difficulty: 2/5

In a subclass, override `add` so it returns `""`

def add(self):
    return ""
Example 3Difficulty: 3/5

Inside an overriding `add`, call the parent implementation and store it in `base_result`

base_result = super().add()

+ 28 more exercises

Quick Reference
Python Classes: self, super(), __repr__ vs __str__, inheritance Cheat Sheet →

Copy-ready syntax examples for quick lookup

Further Reading

  • Facade Pattern in Godot 4 GDScript: Taming "End Turn" Spaghetti12 min read

Related Design Patterns

Observer PatternStrategy PatternFactory Pattern

Start practicing Python Classes: self, super(), __repr__ vs __str__, inheritance

Free daily exercises with spaced repetition. No credit card required.

← Back to Python Syntax Practice
Syntax Cache

Build syntax muscle memory with spaced repetition.

Product

  • Pricing
  • Our Method
  • Daily Practice
  • Design Patterns
  • Interview Prep

Resources

  • Blog
  • Compare
  • Cheat Sheets
  • Vibe Coding
  • Muscle Memory

Languages

  • Python
  • JavaScript
  • TypeScript
  • Rust
  • SQL
  • GDScript

Legal

  • Terms
  • Privacy
  • Contact

© 2026 Syntax Cache

Cancel anytime in 2 clicks. Keep access until the end of your billing period.

No refunds for partial billing periods.