Object-Oriented and Advanced Python
Structure larger programs with classes, iterators, decorators, and typing.
Content
Constructors and __init__
Versions:
Watch & Learn
AI-discovered learning video
Sign in to watch the learning video for this topic.
Constructors and init in Python — The CS50 Way
You already know classes and objects, and you've been messing with methods and attributes. Now let's give our objects a personality from day one.
Why this matters (quick refresher)
You learned in the previous sections how to define a class and call methods on objects. But how do you make sure every new object starts in a valid, ready-to-go state? That's the constructor's job. In Python, that job is usually done by the special method init.
Think of classes like blueprints and init like the factory worker who bolts the engine in, fills the oil, and hands you a fully assembled car. Without init, you get a shell and then have to remember to attach the wheels later — messy.
What is init?
- init is the initializer (often called the constructor) for an instance.
- It runs right after Python creates a new object, letting you set up attributes and enforce invariants.
- Its signature always starts with self:
def __init__(self, ...).
Micro explanation: init does not create the object. Python first allocates the object, then calls init to initialize it. If you want to mess with allocation itself, meet new (advanced, rarely needed).
Basic example (the warm-up)
class Student:
def __init__(self, name, year):
self.name = name
self.year = year
s = Student('Ava', 2)
print(s.name) # Ava
print(s.year) # 2
self.nameandself.yearare instance attributes created during construction.- After
s = Student('Ava', 2),sis ready to use: attributes exist and are meaningful.
Why not set attributes later?
Because forgetting to set them causes AttributeError at runtime. Constructors centralize setup and make code safer and more readable.
Common constructor patterns and pitfalls
1) Default arguments
class Counter:
def __init__(self, start=0):
self.count = start
You can provide defaults so callers get sensible behavior without passing every argument.
2) Avoid mutable default arguments
Bad:
class Bag:
def __init__(self, items=[]):
self.items = items
Good:
class Bag:
def __init__(self, items=None):
if items is None:
items = []
self.items = items
Why: A mutable default gets shared across all instances.
3) Validation inside init
class Person:
def __init__(self, age):
if age < 0:
raise ValueError('age must be >= 0')
self.age = age
Constructors are an excellent place to validate input early.
Advanced: multiple constructors and @classmethod
Sometimes you want alternate ways to create objects, e.g., from a dict or a JSON string.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
@classmethod
def from_tuple(cls, t):
return cls(t[0], t[1])
p = Point.from_tuple((3, 4))
from_tupleis not a special method — it's just a classmethod that returns a new instance. Very handy for named constructors.
Inheritance and super().init
If your class inherits from a parent that needs setup, call super().__init__().
class Animal:
def __init__(self, species):
self.species = species
class Dog(Animal):
def __init__(self, name):
super().__init__('Canis familiaris')
self.name = name
d = Dog('Rex')
print(d.species, d.name) # Canis familiaris Rex
super().__init__()ensures the base class gets initialized too.- Forgetting to call it can leave base attributes uninitialized.
init vs new (short, CS50-friendly)
__new__(cls, ...)— actually creates and returns the new instance (rarely overridden).__init__(self, ...)— initializes the instance returned by__new__and must return None.
You will almost always use init. Override new only for advanced use-cases like immutable singletons or custom memory allocation.
Comparison table (quick scan)
| Role | Typical method | When used |
|---|---|---|
| Create object | new | Very rarely (advanced) |
| Initialize object | init | Almost always |
| Alternative constructors | @classmethod, factory functions | When multiple creation APIs are needed |
Little freakouts and neat tricks
- Want optional flexibility? Use
*args/**kwargsto accept variable input and pass tosuper()or other factories. - Need immutability? Initialize attributes and avoid setters; for full immutability, explore
@dataclass(frozen=True)later. - Want readable error messages? Validate early in init so bugs surface at object creation, not later.
Quick checklist when writing an init
- Give every instance a valid, consistent state.
- Avoid mutable defaults.
- Validate critical inputs (raise ValueError/TypeError early).
- Call
super().__init__()when inheriting. - Keep init focused — heavy logic belongs in other methods or helper functions.
Final recap — what to remember
- init sets up instances. It runs after the object is created so attributes exist immediately.
- Use init to centralize state and validation. This makes your objects reliable and bug-resistant.
- For alternate ways to create objects, prefer @classmethods (named constructors) rather than overloading init with many different argument shapes.
"This is the moment where the concept finally clicks." — When you see a class that always sets its attributes in init, you’ll stop asking whether an object is 'ready' and start trusting it.
Key takeaways:
- init = initializer; keep it clear and defensive.
- Avoid mutable defaults.
- Use classmethods for alternate constructors and super() for inheritance.
Go try it: refactor an earlier class you wrote (from Methods and Attributes) to use a proper init. Your future self will send you a thank-you emoji.
Comments (0)
Please sign in to leave a comment.
No comments yet. Be the first to comment!