Can you write this from memory?
Write the class header for a class named `Item` (header only, no body)
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:
selfis the instance—always the first parameter in methodssuper().__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
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.
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
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.
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()
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:
| Method | Purpose | Called 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:
| Method | Enables |
|---|---|
__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
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
Design a class that validates age on assignment and exposes it as a property.
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
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
class Dog:
def __init__(self, name):
self.name = name # Instance attribute
def bark(self):
return f"{self.name} barks!"# 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 listclass Animal:
def __init__(self, name):
self.name = name
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name) # Call parent __init__
self.breed = breedclass 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# Wrong: calling method on class
Dog.bark() # TypeError: missing argument "self"
# Right: call on instance
fido = Dog("Fido")
fido.bark() # Works!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 = valueclass 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.yfrom 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
Write the method header for an instance method named `add` (header only, no body)
def add(self):In a subclass, override `add` so it returns `""`
def add(self):
return ""Inside an overriding `add`, call the parent implementation and store it in `base_result`
base_result = super().add()+ 28 more exercises
Copy-ready syntax examples for quick lookup