1. Introduction
So you’ve learned about classes, objects, the three pillars of OOP, and even touched on composition vs inheritance. That’s a solid foundation. But Python has a few more tricks up its sleeve that make working with objects feel natural and "Pythonic."
Let’s dive into three topics that often get glossed over but are essential for writing clean, professional Python code.
2. The @property Decorator
Remember encapsulation? We learned that we can make attributes "private" using double underscores to protect them from outside interference. But here’s the thing: sometimes you do need controlled access to those private attributes.
In languages like Java, you’d write explicit getX() and setX() methods. It works, but it’s verbose and clunky:
# The "Java-style" approach (works, but not Pythonic)
class Circle:
def __init__(self, radius):
self.__radius = radius
def get_radius(self):
return self.__radius
def set_radius(self, value):
if value > 0:
self.__radius = value
else:
raise ValueError("Radius must be positive")
# Usage feels awkward
c = Circle(5)
print(c.get_radius()) # 5
c.set_radius(10)
Python offers a more elegant solution: the @property decorator. It lets you define methods that look like simple attribute access but actually run your custom code behind the scenes.
2.1. Creating a Read-Only Property
class Circle:
def __init__(self, radius):
self._radius = radius # Single underscore: "protected" by convention
@property
def radius(self):
"""The radius property (read-only for now)."""
return self._radius
@property
def area(self):
"""Calculated property — no stored value needed."""
return 3.14159 * self._radius ** 2
# Usage feels natural
c = Circle(5)
print(c.radius) # 5 — looks like an attribute, but it's a method!
print(c.area) # 78.53975 — calculated on the fly
Notice how we access radius and area without parentheses. From the outside, they look like regular attributes. But behind the scenes, Python is calling our methods.
2.2. Adding a Setter: Controlled Write Access
What if we want to allow changing the radius, but with validation? We add a setter using @property_name.setter:
class Circle:
def __init__(self, radius):
self._radius = None # Will be set by the setter
self.radius = radius # Use the setter for validation
@property
def radius(self):
"""Get the radius."""
return self._radius
@radius.setter
def radius(self, value):
"""Set the radius with validation."""
if value <= 0:
raise ValueError("Radius must be positive")
self._radius = value
@property
def area(self):
"""Calculated property."""
return 3.14159 * self._radius ** 2
# Now we have validated attribute access
c = Circle(5)
print(c.radius) # 5
c.radius = 10 # Works fine
print(c.radius) # 10
c.radius = -5 # Raises ValueError: Radius must be positive
2.3. The Deleter: Cleaning Up
For completeness, you can also define what happens when someone tries to delete the attribute:
@radius.deleter
def radius(self):
"""Handle deletion of radius."""
print("Deleting radius...")
self._radius = None
# Usage
del c.radius # Prints: Deleting radius...
|
Note
|
The deleter is rarely needed, but it’s available when you need it. |
2.4. Why Use Properties?
Properties offer several advantages:
-
Clean interface: Users of your class interact with simple attributes, not method calls
-
Validation: You can enforce rules when values are set
-
Calculated attributes: Derive values on-the-fly without storing them
-
Backward compatibility: You can start with a simple attribute and later add a property without changing the interface
2.5. Complete Example: Temperature Converter
class Temperature:
"""A temperature that can be accessed in Celsius or Fahrenheit."""
def __init__(self, celsius=0):
self._celsius = celsius
@property
def celsius(self):
"""Temperature in Celsius."""
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Temperature cannot be below absolute zero")
self._celsius = value
@property
def fahrenheit(self):
"""Temperature in Fahrenheit (calculated)."""
return (self._celsius * 9/5) + 32
@fahrenheit.setter
def fahrenheit(self, value):
"""Set temperature using Fahrenheit."""
celsius_value = (value - 32) * 5/9
if celsius_value < -273.15:
raise ValueError("Temperature cannot be below absolute zero")
self._celsius = celsius_value
def __repr__(self):
return f"Temperature({self._celsius}°C / {self.fahrenheit}°F)"
# Usage
temp = Temperature(25)
print(temp) # Temperature(25°C / 77.0°F)
temp.fahrenheit = 100
print(temp) # Temperature(37.77...°C / 100°F)
print(temp.celsius) # 37.77...
print(temp.fahrenheit) # 100
Both celsius and fahrenheit feel like simple attributes, but they’re actually properties with logic behind them. The user doesn’t need to know or care about the implementation.
3. Understanding super()
We touched on inheritance and mentioned super(), but let’s really dig into what it does and why it matters.
When a child class inherits from a parent, sometimes you want to extend the parent’s behavior rather than completely replace it. That’s where super() comes in.
3.1. The Problem: Repeating Yourself
Imagine you’re building on the Animal example:
class Animal:
def __init__(self, name, age):
self.name = name
self.age = age
self.is_alive = True
def speak(self):
raise NotImplementedError("Subclass must implement")
class Dog(Animal):
def __init__(self, name, age, breed):
# Without super(), you'd have to repeat the parent's work:
self.name = name # Duplicated!
self.age = age # Duplicated!
self.is_alive = True # Duplicated!
self.breed = breed # Only this is new
def speak(self):
return f"{self.name} says Woof!"
This works, but it violates DRY (Don’t Repeat Yourself). If Animal.init changes, you’d have to update every child class. That’s a maintenance nightmare.
3.2. The Solution: Using super()
class Animal:
def __init__(self, name, age):
self.name = name
self.age = age
self.is_alive = True
def speak(self):
raise NotImplementedError("Subclass must implement")
def describe(self):
return f"{self.name} is {self.age} years old"
class Dog(Animal):
def __init__(self, name, age, breed):
super().__init__(name, age) # Call the parent's __init__
self.breed = breed # Add dog-specific attribute
def speak(self):
return f"{self.name} says Woof!"
def describe(self):
# Extend the parent's describe method
base_description = super().describe()
return f"{base_description} and is a {self.breed}"
# Usage
rex = Dog("Rex", 5, "German Shepherd")
print(rex.name) # Rex (set by parent)
print(rex.age) # 5 (set by parent)
print(rex.is_alive) # True (set by parent)
print(rex.breed) # German Shepherd (set by child)
print(rex.speak()) # Rex says Woof!
print(rex.describe()) # Rex is 5 years old and is a German Shepherd
3.3. What super() Actually Returns
When you call super(), Python returns a special proxy object that delegates method calls to the parent class. It’s essentially saying: "Give me a way to call methods from my parent."
super().__init__(name, age)
This is equivalent to the older, more explicit syntax:
Animal.__init__(self, name, age)
|
Tip
|
The
|
3.4. Extending Methods Beyond init
You can use super() in any method, not just init:
class Animal:
def eat(self, food):
print(f"{self.name} is eating {food}")
self.hunger = 0
class Dog(Animal):
def eat(self, food):
if food == "chocolate":
print(f"No! Chocolate is toxic for dogs!")
return
super().eat(food) # Call parent's eat method
print(f"{self.name} wags tail happily")
rex = Dog("Rex", 5, "German Shepherd")
rex.hunger = 100
rex.eat("chocolate")
# Output: No! Chocolate is toxic for dogs!
rex.eat("kibble")
# Output: Rex is eating kibble
# Rex wags tail happily
3.5. Practical Example: Building a User System
class User:
"""Base user class."""
def __init__(self, username, email):
self.username = username
self.email = email
self.is_active = True
self.created_at = "2025-01-01" # Simplified
def get_permissions(self):
return ["read"]
def __repr__(self):
return f"User({self.username})"
class AdminUser(User):
"""Admin with elevated permissions."""
def __init__(self, username, email, admin_level=1):
super().__init__(username, email) # Set up base user stuff
self.admin_level = admin_level
def get_permissions(self):
# Start with base permissions, then add more
base_permissions = super().get_permissions()
admin_permissions = ["write", "delete"]
if self.admin_level >= 2:
admin_permissions.append("manage_users")
return base_permissions + admin_permissions
def __repr__(self):
return f"AdminUser({self.username}, level={self.admin_level})"
class SuperAdmin(AdminUser):
"""Super admin with all permissions."""
def __init__(self, username, email):
super().__init__(username, email, admin_level=3)
def get_permissions(self):
return super().get_permissions() + ["system_config", "view_logs"]
# Usage
regular = User("alice", "alice@example.com")
admin = AdminUser("bob", "bob@example.com", admin_level=2)
super_admin = SuperAdmin("charlie", "charlie@example.com")
print(regular.get_permissions())
# ['read']
print(admin.get_permissions())
# ['read', 'write', 'delete', 'manage_users']
print(super_admin.get_permissions())
# ['read', 'write', 'delete', 'manage_users', 'system_config', 'view_logs']
Each level builds on the previous one, and super() makes it seamless.
4. Duck Typing
The presentation mentioned duck typing briefly, but it deserves a deeper look because it’s fundamental to how Python thinks about objects.
4.1. The Philosophy
The name comes from the saying:
If it walks like a duck and quacks like a duck, then it probably is a duck.
In Python terms: we don’t care what type an object is, only what it can do.
Unlike languages like Java or C++, Python doesn’t require objects to inherit from a common parent or implement a formal interface. If an object has the method you need, you can use it.
4.2. A Simple Example
class Duck:
def speak(self):
return "Quack!"
def swim(self):
return "Duck is swimming"
class Person:
def speak(self):
return "Hello!"
def swim(self):
return "Person is swimming"
class Robot:
def speak(self):
return "Beep boop!"
def swim(self):
return "Robot is short-circuiting!"
def make_it_speak(thing):
"""This function doesn't care about the type of 'thing'."""
print(thing.speak())
def pool_party(participants):
"""Everyone goes swimming!"""
for participant in participants:
print(participant.swim())
# None of these classes inherit from each other
# But they all work because they have the same methods
duck = Duck()
person = Person()
robot = Robot()
make_it_speak(duck) # Quack!
make_it_speak(person) # Hello!
make_it_speak(robot) # Beep boop!
pool_party([duck, person, robot])
# Duck is swimming
# Person is swimming
# Robot is short-circuiting!
The make_it_speak function works with any object that has a speak method. It doesn’t check types, it just tries to use the method.
4.3. Duck Typing vs Traditional Polymorphism
With traditional inheritance-based polymorphism, you’d write:
class Speakable:
def speak(self):
raise NotImplementedError
class Duck(Speakable):
def speak(self):
return "Quack!"
class Person(Speakable):
def speak(self):
return "Hello!"
This works, but Python’s duck typing lets you skip the formal hierarchy entirely. Your classes don’t need to know about each other or share a parent.
4.4. Real-World Duck Typing: File-Like Objects
Python’s standard library uses duck typing extensively. Consider reading data:
def process_data(data_source):
"""Process data from anything that has a read() method."""
content = data_source.read()
return content.upper()
# Works with actual files
with open("myfile.txt") as f:
result = process_data(f)
# Works with StringIO (fake file in memory)
from io import StringIO
fake_file = StringIO("hello world")
result = process_data(fake_file) # "HELLO WORLD"
# Works with BytesIO
from io import BytesIO
byte_file = BytesIO(b"hello world")
# (would need .decode() but you get the idea)
The process_data function doesn’t check if it received a "real" file. It just needs something with a read() method.
4.5. Duck Typing with Iteration
You’ve been using duck typing since Chapter 2 without realizing it:
def print_all(items):
"""Print each item. Works with anything iterable."""
for item in items:
print(item)
# All of these work because they're all "iterable"
print_all([1, 2, 3]) # List
print_all((1, 2, 3)) # Tuple
print_all({1, 2, 3}) # Set
print_all("abc") # String
print_all(range(3)) # Range
print_all({"a": 1, "b": 2}) # Dict (iterates keys)
The for loop doesn’t check types. It just needs an object that supports iteration (has iter method).
4.6. Making Your Classes Duck-Type Friendly
Want your custom class to work with for loops? Just implement the right methods:
class Countdown:
"""A countdown that can be iterated."""
def __init__(self, start):
self.start = start
def __iter__(self):
"""Make this class iterable."""
self.current = self.start
return self
def __next__(self):
"""Return the next value."""
if self.current < 0:
raise StopIteration
value = self.current
self.current -= 1
return value
# Now it works with for loops!
for num in Countdown(5):
print(num)
# Output: 5, 4, 3, 2, 1, 0
# And with list()
numbers = list(Countdown(3)) # [3, 2, 1, 0]
4.7. Handling Duck Typing Failures
What happens when duck typing fails? The object doesn’t have the method you expected:
class Rock:
pass # Rocks don't speak
def make_it_speak(thing):
print(thing.speak())
rock = Rock()
make_it_speak(rock) # AttributeError: 'Rock' object has no attribute 'speak'
You have options for handling this:
4.7.1. Option 1: EAFP (Easier to Ask Forgiveness than Permission)
This is the Pythonic way:
def make_it_speak(thing):
try:
print(thing.speak())
except AttributeError:
print(f"{type(thing).__name__} cannot speak")
make_it_speak(Rock()) # "Rock cannot speak"
4.7.2. Option 2: LBYL (Look Before You Leap)
Check first using hasattr():
def make_it_speak(thing):
if hasattr(thing, 'speak'):
print(thing.speak())
else:
print(f"{type(thing).__name__} cannot speak")
4.7.3. Option 3: Use callable() for Methods
def make_it_speak(thing):
speak_method = getattr(thing, 'speak', None)
if callable(speak_method):
print(speak_method())
else:
print(f"{type(thing).__name__} cannot speak")
|
Note
|
EAFP (try/except) is generally preferred in Python because:
|
4.8. Duck Typing Summary
| Concept | Description |
|---|---|
Core Idea |
Care about behavior, not type |
Benefit |
Flexible, loosely-coupled code |
Risk |
Runtime errors if object lacks expected method |
Best Practice |
Use try/except (EAFP) for graceful handling |
Common Uses |
Iteration, file operations, context managers |
5. Putting It All Together
Let’s build a small example that combines all three concepts:
class DataSource:
"""Base class for data sources using properties and meant for duck typing."""
def __init__(self, name):
self._name = name
self._data = []
@property
def name(self):
return self._name
@property
def record_count(self):
"""Calculated property."""
return len(self._data)
def read(self):
"""Duck typing target: anything with read() can be a data source."""
raise NotImplementedError
class CSVSource(DataSource):
"""Reads data from a CSV-like format."""
def __init__(self, name, raw_text):
super().__init__(name) # Call parent's __init__
self._raw_text = raw_text
= Python OOP: The Missing Pieces
:toc:
:toc-placement: left
:toclevels: 3
:sectnums:
:source-highlighter: pygments
== Introduction
So you've learned about classes, objects, the three pillars of OOP, and even touched on composition vs inheritance. That's a solid foundation. But Python has a few more tricks up its sleeve that make working with objects feel natural and "Pythonic."
Let's dive into three topics that often get glossed over but are essential for writing clean, professional Python code.
== The @property Decorator
Remember encapsulation? We learned that we can make attributes "private" using double underscores to protect them from outside interference. But here's the thing: sometimes you _do_ need controlled access to those private attributes.
In languages like Java, you'd write explicit `getX()` and `setX()` methods. It works, but it's verbose and clunky:
[source,python]
The "Java-style" approach (works, but not Pythonic)
class Circle: def init(self, radius): self.__radius = radius
def get_radius(self):
return self.__radius
def set_radius(self, value):
if value > 0:
self.__radius = value
else:
raise ValueError("Radius must be positive")
Usage feels awkward
c = Circle(5) print(c.get_radius()) # 5 c.set_radius(10)
Python offers a more elegant solution: the `@property` decorator. It lets you define methods that _look_ like simple attribute access but actually run your custom code behind the scenes. === Creating a Read-Only Property [source,python]
class Circle: def init(self, radius): self._radius = radius # Single underscore: "protected" by convention
@property
def radius(self):
"""The radius property (read-only for now)."""
return self._radius
@property
def area(self):
"""Calculated property — no stored value needed."""
return 3.14159 * self._radius ** 2
Usage feels natural
c = Circle(5) print(c.radius) # 5 — looks like an attribute, but it’s a method! print(c.area) # 78.53975 — calculated on the fly
Notice how we access `radius` and `area` without parentheses. From the outside, they look like regular attributes. But behind the scenes, Python is calling our methods. === Adding a Setter: Controlled Write Access What if we want to allow changing the radius, but with validation? We add a setter using `@property_name.setter`: [source,python]
class Circle: def init(self, radius): self._radius = None # Will be set by the setter self.radius = radius # Use the setter for validation
@property
def radius(self):
"""Get the radius."""
return self._radius
@radius.setter
def radius(self, value):
"""Set the radius with validation."""
if value <= 0:
raise ValueError("Radius must be positive")
self._radius = value
@property
def area(self):
"""Calculated property."""
return 3.14159 * self._radius ** 2
Now we have validated attribute access
c = Circle(5) print(c.radius) # 5
c.radius = 10 # Works fine print(c.radius) # 10
c.radius = -5 # Raises ValueError: Radius must be positive
=== The Deleter: Cleaning Up For completeness, you can also define what happens when someone tries to delete the attribute: [source,python]
@radius.deleter def radius(self): """Handle deletion of radius.""" print("Deleting radius…") self._radius = None
Usage
del c.radius # Prints: Deleting radius…
[NOTE] ==== The deleter is rarely needed, but it's available when you need it. ==== === Why Use Properties? Properties offer several advantages: * **Clean interface**: Users of your class interact with simple attributes, not method calls * **Validation**: You can enforce rules when values are set * **Calculated attributes**: Derive values on-the-fly without storing them * **Backward compatibility**: You can start with a simple attribute and later add a property without changing the interface === Complete Example: Temperature Converter [source,python]
class Temperature: """A temperature that can be accessed in Celsius or Fahrenheit."""
def __init__(self, celsius=0):
self._celsius = celsius
@property
def celsius(self):
"""Temperature in Celsius."""
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Temperature cannot be below absolute zero")
self._celsius = value
@property
def fahrenheit(self):
"""Temperature in Fahrenheit (calculated)."""
return (self._celsius * 9/5) + 32
@fahrenheit.setter
def fahrenheit(self, value):
"""Set temperature using Fahrenheit."""
celsius_value = (value - 32) * 5/9
if celsius_value < -273.15:
raise ValueError("Temperature cannot be below absolute zero")
self._celsius = celsius_value
def __repr__(self):
return f"Temperature({self._celsius}°C / {self.fahrenheit}°F)"
Usage
temp = Temperature(25) print(temp) # Temperature(25°C / 77.0°F)
temp.fahrenheit = 100 print(temp) # Temperature(37.77…°C / 100°F)
print(temp.celsius) # 37.77… print(temp.fahrenheit) # 100
Both `celsius` and `fahrenheit` feel like simple attributes, but they're actually properties with logic behind them. The user doesn't need to know or care about the implementation. == Understanding super() We touched on inheritance and mentioned `super()`, but let's really dig into what it does and why it matters. When a child class inherits from a parent, sometimes you want to _extend_ the parent's behavior rather than completely replace it. That's where `super()` comes in. === The Problem: Repeating Yourself Imagine you're building on the `Animal` example: [source,python]
class Animal: def init(self, name, age): self.name = name self.age = age self.is_alive = True
def speak(self):
raise NotImplementedError("Subclass must implement")
class Dog(Animal): def init(self, name, age, breed): # Without super(), you’d have to repeat the parent’s work: self.name = name # Duplicated! self.age = age # Duplicated! self.is_alive = True # Duplicated! self.breed = breed # Only this is new
def speak(self):
return f"{self.name} says Woof!"
This works, but it violates DRY (Don't Repeat Yourself). If `Animal.__init__` changes, you'd have to update every child class. That's a maintenance nightmare. === The Solution: Using super() [source,python]
class Animal: def init(self, name, age): self.name = name self.age = age self.is_alive = True
def speak(self):
raise NotImplementedError("Subclass must implement")
def describe(self):
return f"{self.name} is {self.age} years old"
class Dog(Animal): def init(self, name, age, breed): super().init(name, age) # Call the parent’s init self.breed = breed # Add dog-specific attribute
def speak(self):
return f"{self.name} says Woof!"
def describe(self):
# Extend the parent's describe method
base_description = super().describe()
return f"{base_description} and is a {self.breed}"
Usage
rex = Dog("Rex", 5, "German Shepherd")
print(rex.name) # Rex (set by parent) print(rex.age) # 5 (set by parent) print(rex.is_alive) # True (set by parent) print(rex.breed) # German Shepherd (set by child)
print(rex.speak()) # Rex says Woof! print(rex.describe()) # Rex is 5 years old and is a German Shepherd
=== What super() Actually Returns When you call `super()`, Python returns a special proxy object that delegates method calls to the parent class. It's essentially saying: "Give me a way to call methods from my parent." [source,python]
super().init(name, age)
This is equivalent to the older, more explicit syntax: [source,python]
Animal.init(self, name, age)
[TIP] ==== The `super()` syntax is preferred because: * It's cleaner and more readable * It works correctly with multiple inheritance (more advanced topic) * If you rename the parent class, you don't have to update the child ==== === Extending Methods Beyond __init__ You can use `super()` in any method, not just `__init__`: [source,python]
class Animal: def eat(self, food): print(f"{self.name} is eating {food}") self.hunger = 0
class Dog(Animal): def eat(self, food): if food == "chocolate": print(f"No! Chocolate is toxic for dogs!") return super().eat(food) # Call parent’s eat method print(f"{self.name} wags tail happily")
rex = Dog("Rex", 5, "German Shepherd") rex.hunger = 100
rex.eat("chocolate") # Output: No! Chocolate is toxic for dogs!
rex.eat("kibble") # Output: Rex is eating kibble # Rex wags tail happily
=== Practical Example: Building a User System [source,python]
class User: """Base user class."""
def __init__(self, username, email):
self.username = username
self.email = email
self.is_active = True
self.created_at = "2025-01-01" # Simplified
def get_permissions(self):
return ["read"]
def __repr__(self):
return f"User({self.username})"
class AdminUser(User): """Admin with elevated permissions."""
def __init__(self, username, email, admin_level=1):
super().__init__(username, email) # Set up base user stuff
self.admin_level = admin_level
def get_permissions(self):
# Start with base permissions, then add more
base_permissions = super().get_permissions()
admin_permissions = ["write", "delete"]
if self.admin_level >= 2:
admin_permissions.append("manage_users")
return base_permissions + admin_permissions
def __repr__(self):
return f"AdminUser({self.username}, level={self.admin_level})"
class SuperAdmin(AdminUser): """Super admin with all permissions."""
def __init__(self, username, email):
super().__init__(username, email, admin_level=3)
def get_permissions(self):
return super().get_permissions() + ["system_config", "view_logs"]
Usage
regular = User("alice", "alice@example.com") admin = AdminUser("bob", "bob@example.com", admin_level=2) super_admin = SuperAdmin("charlie", "charlie@example.com")
print(regular.get_permissions()) # ['read']
print(admin.get_permissions()) # ['read', 'write', 'delete', 'manage_users']
print(super_admin.get_permissions()) # ['read', 'write', 'delete', 'manage_users', 'system_config', 'view_logs']
Each level builds on the previous one, and `super()` makes it seamless. == Duck Typing The presentation mentioned duck typing briefly, but it deserves a deeper look because it's fundamental to how Python thinks about objects. === The Philosophy The name comes from the saying: [quote] ____ If it walks like a duck and quacks like a duck, then it probably is a duck. ____ In Python terms: *we don't care what type an object _is_, only what it can _do_.* Unlike languages like Java or C++, Python doesn't require objects to inherit from a common parent or implement a formal interface. If an object has the method you need, you can use it. === A Simple Example [source,python]
class Duck: def speak(self): return "Quack!"
def swim(self):
return "Duck is swimming"
class Person: def speak(self): return "Hello!"
def swim(self):
return "Person is swimming"
class Robot: def speak(self): return "Beep boop!"
def swim(self):
return "Robot is short-circuiting!"
def make_it_speak(thing): """This function doesn’t care about the type of 'thing'.""" print(thing.speak())
def pool_party(participants): """Everyone goes swimming!""" for participant in participants: print(participant.swim())
None of these classes inherit from each other
But they all work because they have the same methods
duck = Duck() person = Person() robot = Robot()
make_it_speak(duck) # Quack! make_it_speak(person) # Hello! make_it_speak(robot) # Beep boop!
pool_party([duck, person, robot]) # Duck is swimming # Person is swimming # Robot is short-circuiting!
The `make_it_speak` function works with _any_ object that has a `speak` method. It doesn't check types, it just tries to use the method. === Duck Typing vs Traditional Polymorphism With traditional inheritance-based polymorphism, you'd write: [source,python]
class Speakable: def speak(self): raise NotImplementedError
class Duck(Speakable): def speak(self): return "Quack!"
class Person(Speakable): def speak(self): return "Hello!"
This works, but Python's duck typing lets you skip the formal hierarchy entirely. Your classes don't need to know about each other or share a parent. === Real-World Duck Typing: File-Like Objects Python's standard library uses duck typing extensively. Consider reading data: [source,python]
def process_data(data_source): """Process data from anything that has a read() method.""" content = data_source.read() return content.upper()
Works with actual files
with open("myfile.txt") as f: result = process_data(f)
Works with StringIO (fake file in memory)
from io import StringIO fake_file = StringIO("hello world") result = process_data(fake_file) # "HELLO WORLD"
Works with BytesIO
from io import BytesIO byte_file = BytesIO(b"hello world") # (would need .decode() but you get the idea)
The `process_data` function doesn't check if it received a "real" file. It just needs something with a `read()` method. === Duck Typing with Iteration You've been using duck typing since Chapter 2 without realizing it: [source,python]
def print_all(items): """Print each item. Works with anything iterable.""" for item in items: print(item)
All of these work because they’re all "iterable"
print_all([1, 2, 3]) # List print_all1, 2, 3 # Tuple print_all({1, 2, 3}) # Set print_all("abc") # String print_all(range(3)) # Range print_all({"a": 1, "b": 2}) # Dict (iterates keys)
The `for` loop doesn't check types. It just needs an object that supports iteration (has `__iter__` method). === Making Your Classes Duck-Type Friendly Want your custom class to work with `for` loops? Just implement the right methods: [source,python]
class Countdown: """A countdown that can be iterated."""
def __init__(self, start):
self.start = start
def __iter__(self):
"""Make this class iterable."""
self.current = self.start
return self
def __next__(self):
"""Return the next value."""
if self.current < 0:
raise StopIteration
value = self.current
self.current -= 1
return value
Now it works with for loops!
for num in Countdown(5): print(num) # Output: 5, 4, 3, 2, 1, 0
And with list()
numbers = list(Countdown(3)) # [3, 2, 1, 0]
=== Handling Duck Typing Failures What happens when duck typing fails? The object doesn't have the method you expected: [source,python]
class Rock: pass # Rocks don’t speak
def make_it_speak(thing): print(thing.speak())
rock = Rock() make_it_speak(rock) # AttributeError: 'Rock' object has no attribute 'speak'
You have options for handling this: ==== Option 1: EAFP (Easier to Ask Forgiveness than Permission) This is the Pythonic way: [source,python]
def make_it_speak(thing): try: print(thing.speak()) except AttributeError: print(f"{type(thing).name} cannot speak")
make_it_speak(Rock()) # "Rock cannot speak"
==== Option 2: LBYL (Look Before You Leap) Check first using `hasattr()`: [source,python]
def make_it_speak(thing): if hasattr(thing, 'speak'): print(thing.speak()) else: print(f"{type(thing).name} cannot speak")
==== Option 3: Use callable() for Methods [source,python]
def make_it_speak(thing): speak_method = getattr(thing, 'speak', None) if callable(speak_method): print(speak_method()) else: print(f"{type(thing).name} cannot speak")
[NOTE] ==== EAFP (try/except) is generally preferred in Python because: * It's often faster when the method usually exists * It's more Pythonic * It handles edge cases better ==== === Duck Typing Summary [cols="1,3"] |=== |Concept |Description |**Core Idea** |Care about behavior, not type |**Benefit** |Flexible, loosely-coupled code |**Risk** |Runtime errors if object lacks expected method |**Best Practice** |Use try/except (EAFP) for graceful handling |**Common Uses** |Iteration, file operations, context managers |=== == Putting It All Together Let's build a small example that combines all three concepts: [source,python]
class DataSource: """Base class for data sources using properties and meant for duck typing."""
def __init__(self, name):
self._name = name
self._data = []
@property
def name(self):
return self._name
@property
def record_count(self):
"""Calculated property."""
return len(self._data)
def read(self):
"""Duck typing target: anything with read() can be a data source."""
raise NotImplementedError
class CSVSource(DataSource): """Reads data from a CSV-like format."""
def __init__(self, name, raw_text):
super().__init__(name) # Call parent's __init__
self._raw_text = raw_text
def read(self):
"""Parse CSV and return records."""
lines = self._raw_text.strip().split('\n')
headers = lines[0].split(',')
self._data = []
for line in lines[1:]:
values = line.split(',')
record = dict(zip(headers, values))
self._data.append(record)
return self._data
class JSONSource(DataSource): """Reads data from JSON format."""
def __init__(self, name, json_data):
super().__init__(name)
self._json_data = json_data
def read(self):
"""Parse JSON and return records."""
import json
self._data = json.loads(self._json_data)
return self._data
def analyze_data(source): """ Works with ANY object that has read() and record_count. This is duck typing in action. """ try: data = source.read() print(f"Source: {source.name}") print(f"Records: {source.record_count}") print(f"First record: {data[0] if data else 'No data'}") print() except AttributeError as e: print(f"Invalid data source: {e}")
Usage
csv_data = """name,age,city Alice,30,New York Bob,25,Boston Charlie,35,Chicago"""
json_data = '[{"name": "Diana", "score": 95}, {"name": "Eve", "score": 87}]'
csv_source = CSVSource("Employee CSV", csv_data) json_source = JSONSource("Scores JSON", json_data)
Both work with the same function thanks to duck typing
analyze_data(csv_source) # Source: Employee CSV # Records: 3 # First record: {'name': 'Alice', 'age': '30', 'city': 'New York'}
analyze_data(json_source) # Source: Scores JSON # Records: 2= Python OOP: The Missing Pieces :toc: :toc-placement: left :toclevels: 3 :sectnums: :source-highlighter: pygments
1. Introduction
So you’ve learned about classes, objects, the three pillars of OOP, and even touched on composition vs inheritance. That’s a solid foundation. But Python has a few more tricks up its sleeve that make working with objects feel natural and "Pythonic."
Let’s dive into three topics that often get glossed over but are essential for writing clean, professional Python code.
2. The @property Decorator
Remember encapsulation? We learned that we can make attributes "private" using double underscores to protect them from outside interference. But here’s the thing: sometimes you do need controlled access to those private attributes.
In languages like Java, you’d write explicit getX() and setX() methods. It works, but it’s verbose and clunky:
# The "Java-style" approach (works, but not Pythonic)
class Circle:
def __init__(self, radius):
self.__radius = radius
def get_radius(self):
return self.__radius
def set_radius(self, value):
if value > 0:
self.__radius = value
else:
raise ValueError("Radius must be positive")
# Usage feels awkward
c = Circle(5)
print(c.get_radius()) # 5
c.set_radius(10)
Python offers a more elegant solution: the @property decorator. It lets you define methods that look like simple attribute access but actually run your custom code behind the scenes.
2.1. Creating a Read-Only Property
class Circle:
def __init__(self, radius):
self._radius = radius # Single underscore: "protected" by convention
@property
def radius(self):
"""The radius property (read-only for now)."""
return self._radius
@property
def area(self):
"""Calculated property — no stored value needed."""
return 3.14159 * self._radius ** 2
# Usage feels natural
c = Circle(5)
print(c.radius) # 5 — looks like an attribute, but it's a method!
print(c.area) # 78.53975 — calculated on the fly
Notice how we access radius and area without parentheses. From the outside, they look like regular attributes. But behind the scenes, Python is calling our methods.
2.2. Adding a Setter: Controlled Write Access
What if we want to allow changing the radius, but with validation? We add a setter using @property_name.setter:
class Circle:
def __init__(self, radius):
self._radius = None # Will be set by the setter
self.radius = radius # Use the setter for validation
@property
def radius(self):
"""Get the radius."""
return self._radius
@radius.setter
def radius(self, value):
"""Set the radius with validation."""
if value <= 0:
raise ValueError("Radius must be positive")
self._radius = value
@property
def area(self):
"""Calculated property."""
return 3.14159 * self._radius ** 2
# Now we have validated attribute access
c = Circle(5)
print(c.radius) # 5
c.radius = 10 # Works fine
print(c.radius) # 10
c.radius = -5 # Raises ValueError: Radius must be positive
2.3. The Deleter: Cleaning Up
For completeness, you can also define what happens when someone tries to delete the attribute:
@radius.deleter
def radius(self):
"""Handle deletion of radius."""
print("Deleting radius...")
self._radius = None
# Usage
del c.radius # Prints: Deleting radius...
|
Note
|
The deleter is rarely needed, but it’s available when you need it. |
2.4. Why Use Properties?
Properties offer several advantages:
-
Clean interface: Users of your class interact with simple attributes, not method calls
-
Validation: You can enforce rules when values are set
-
Calculated attributes: Derive values on-the-fly without storing them
-
Backward compatibility: You can start with a simple attribute and later add a property without changing the interface
2.5. Complete Example: Temperature Converter
class Temperature:
"""A temperature that can be accessed in Celsius or Fahrenheit."""
def __init__(self, celsius=0):
self._celsius = celsius
@property
def celsius(self):
"""Temperature in Celsius."""
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Temperature cannot be below absolute zero")
self._celsius = value
@property
def fahrenheit(self):
"""Temperature in Fahrenheit (calculated)."""
return (self._celsius * 9/5) + 32
@fahrenheit.setter
def fahrenheit(self, value):
"""Set temperature using Fahrenheit."""
celsius_value = (value - 32) * 5/9
if celsius_value < -273.15:
raise ValueError("Temperature cannot be below absolute zero")
self._celsius = celsius_value
def __repr__(self):
return f"Temperature({self._celsius}°C / {self.fahrenheit}°F)"
# Usage
temp = Temperature(25)
print(temp) # Temperature(25°C / 77.0°F)
temp.fahrenheit = 100
print(temp) # Temperature(37.77...°C / 100°F)
print(temp.celsius) # 37.77...
print(temp.fahrenheit) # 100
Both celsius and fahrenheit feel like simple attributes, but they’re actually properties with logic behind them. The user doesn’t need to know or care about the implementation.
3. Understanding super()
We touched on inheritance and mentioned super(), but let’s really dig into what it does and why it matters.
When a child class inherits from a parent, sometimes you want to extend the parent’s behavior rather than completely replace it. That’s where super() comes in.
3.1. The Problem: Repeating Yourself
Imagine you’re building on the Animal example:
class Animal:
def __init__(self, name, age):
self.name = name
self.age = age
self.is_alive = True
def speak(self):
raise NotImplementedError("Subclass must implement")
class Dog(Animal):
def __init__(self, name, age, breed):
# Without super(), you'd have to repeat the parent's work:
self.name = name # Duplicated!
self.age = age # Duplicated!
self.is_alive = True # Duplicated!
self.breed = breed # Only this is new
def speak(self):
return f"{self.name} says Woof!"
This works, but it violates DRY (Don’t Repeat Yourself). If Animal.init changes, you’d have to update every child class. That’s a maintenance nightmare.
3.2. The Solution: Using super()
class Animal:
def __init__(self, name, age):
self.name = name
self.age = age
self.is_alive = True
def speak(self):
raise NotImplementedError("Subclass must implement")
def describe(self):
return f"{self.name} is {self.age} years old"
class Dog(Animal):
def __init__(self, name, age, breed):
super().__init__(name, age) # Call the parent's __init__
self.breed = breed # Add dog-specific attribute
def speak(self):
return f"{self.name} says Woof!"
def describe(self):
# Extend the parent's describe method
base_description = super().describe()
return f"{base_description} and is a {self.breed}"
# Usage
rex = Dog("Rex", 5, "German Shepherd")
print(rex.name) # Rex (set by parent)
print(rex.age) # 5 (set by parent)
print(rex.is_alive) # True (set by parent)
print(rex.breed) # German Shepherd (set by child)
print(rex.speak()) # Rex says Woof!
print(rex.describe()) # Rex is 5 years old and is a German Shepherd
3.3. What super() Actually Returns
When you call super(), Python returns a special proxy object that delegates method calls to the parent class. It’s essentially saying: "Give me a way to call methods from my parent."
super().__init__(name, age)
This is equivalent to the older, more explicit syntax:
Animal.__init__(self, name, age)
|
Tip
|
The
|
3.4. Extending Methods Beyond init
You can use super() in any method, not just init:
class Animal:
def eat(self, food):
print(f"{self.name} is eating {food}")
self.hunger = 0
class Dog(Animal):
def eat(self, food):
if food == "chocolate":
print(f"No! Chocolate is toxic for dogs!")
return
super().eat(food) # Call parent's eat method
print(f"{self.name} wags tail happily")
rex = Dog("Rex", 5, "German Shepherd")
rex.hunger = 100
rex.eat("chocolate")
# Output: No! Chocolate is toxic for dogs!
rex.eat("kibble")
# Output: Rex is eating kibble
# Rex wags tail happily
3.5. Practical Example: Building a User System
class User:
"""Base user class."""
def __init__(self, username, email):
self.username = username
self.email = email
self.is_active = True
self.created_at = "2025-01-01" # Simplified
def get_permissions(self):
return ["read"]
def __repr__(self):
return f"User({self.username})"
class AdminUser(User):
"""Admin with elevated permissions."""
def __init__(self, username, email, admin_level=1):
super().__init__(username, email) # Set up base user stuff
self.admin_level = admin_level
def get_permissions(self):
# Start with base permissions, then add more
base_permissions = super().get_permissions()
admin_permissions = ["write", "delete"]
if self.admin_level >= 2:
admin_permissions.append("manage_users")
return base_permissions + admin_permissions
def __repr__(self):
return f"AdminUser({self.username}, level={self.admin_level})"
class SuperAdmin(AdminUser):
"""Super admin with all permissions."""
def __init__(self, username, email):
super().__init__(username, email, admin_level=3)
def get_permissions(self):
return super().get_permissions() + ["system_config", "view_logs"]
# Usage
regular = User("alice", "alice@example.com")
admin = AdminUser("bob", "bob@example.com", admin_level=2)
super_admin = SuperAdmin("charlie", "charlie@example.com")
print(regular.get_permissions())
# ['read']
print(admin.get_permissions())
# ['read', 'write', 'delete', 'manage_users']
print(super_admin.get_permissions())
# ['read', 'write', 'delete', 'manage_users', 'system_config', 'view_logs']
Each level builds on the previous one, and super() makes it seamless.
4. Duck Typing
The presentation mentioned duck typing briefly, but it deserves a deeper look because it’s fundamental to how Python thinks about objects.
4.1. The Philosophy
The name comes from the saying:
If it walks like a duck and quacks like a duck, then it probably is a duck.
In Python terms: we don’t care what type an object is, only what it can do.
Unlike languages like Java or C++, Python doesn’t require objects to inherit from a common parent or implement a formal interface. If an object has the method you need, you can use it.
4.2. A Simple Example
class Duck:
def speak(self):
return "Quack!"
def swim(self):
return "Duck is swimming"
class Person:
def speak(self):
return "Hello!"
def swim(self):
return "Person is swimming"
class Robot:
def speak(self):
return "Beep boop!"
def swim(self):
return "Robot is short-circuiting!"
def make_it_speak(thing):
"""This function doesn't care about the type of 'thing'."""
print(thing.speak())
def pool_party(participants):
"""Everyone goes swimming!"""
for participant in participants:
print(participant.swim())
# None of these classes inherit from each other
# But they all work because they have the same methods
duck = Duck()
person = Person()
robot = Robot()
make_it_speak(duck) # Quack!
make_it_speak(person) # Hello!
make_it_speak(robot) # Beep boop!
pool_party([duck, person, robot])
# Duck is swimming
# Person is swimming
# Robot is short-circuiting!
The make_it_speak function works with any object that has a speak method. It doesn’t check types, it just tries to use the method.
4.3. Duck Typing vs Traditional Polymorphism
With traditional inheritance-based polymorphism, you’d write:
class Speakable:
def speak(self):
raise NotImplementedError
class Duck(Speakable):
def speak(self):
return "Quack!"
class Person(Speakable):
def speak(self):
return "Hello!"
This works, but Python’s duck typing lets you skip the formal hierarchy entirely. Your classes don’t need to know about each other or share a parent.
4.4. Real-World Duck Typing: File-Like Objects
Python’s standard library uses duck typing extensively. Consider reading data:
def process_data(data_source):
"""Process data from anything that has a read() method."""
content = data_source.read()
return content.upper()
# Works with actual files
with open("myfile.txt") as f:
result = process_data(f)
# Works with StringIO (fake file in memory)
from io import StringIO
fake_file = StringIO("hello world")
result = process_data(fake_file) # "HELLO WORLD"
# Works with BytesIO
from io import BytesIO
byte_file = BytesIO(b"hello world")
# (would need .decode() but you get the idea)
The process_data function doesn’t check if it received a "real" file. It just needs something with a read() method.
4.5. Duck Typing with Iteration
You’ve been using duck typing since Chapter 2 without realizing it:
def print_all(items):
"""Print each item. Works with anything iterable."""
for item in items:
print(item)
# All of these work because they're all "iterable"
print_all([1, 2, 3]) # List
print_all((1, 2, 3)) # Tuple
print_all({1, 2, 3}) # Set
print_all("abc") # String
print_all(range(3)) # Range
print_all({"a": 1, "b": 2}) # Dict (iterates keys)
The for loop doesn’t check types. It just needs an object that supports iteration (has iter method).
4.6. Making Your Classes Duck-Type Friendly
Want your custom class to work with for loops? Just implement the right methods:
class Countdown:
"""A countdown that can be iterated."""
def __init__(self, start):
self.start = start
def __iter__(self):
"""Make this class iterable."""
self.current = self.start
return self
def __next__(self):
"""Return the next value."""
if self.current < 0:
raise StopIteration
value = self.current
self.current -= 1
return value
# Now it works with for loops!
for num in Countdown(5):
print(num)
# Output: 5, 4, 3, 2, 1, 0
# And with list()
numbers = list(Countdown(3)) # [3, 2, 1, 0]
4.7. Handling Duck Typing Failures
What happens when duck typing fails? The object doesn’t have the method you expected:
class Rock:
pass # Rocks don't speak
def make_it_speak(thing):
print(thing.speak())
rock = Rock()
make_it_speak(rock) # AttributeError: 'Rock' object has no attribute 'speak'
You have options for handling this:
4.7.1. Option 1: EAFP (Easier to Ask Forgiveness than Permission)
This is the Pythonic way:
def make_it_speak(thing):
try:
print(thing.speak())
except AttributeError:
print(f"{type(thing).__name__} cannot speak")
make_it_speak(Rock()) # "Rock cannot speak"
4.7.2. Option 2: LBYL (Look Before You Leap)
Check first using hasattr():
def make_it_speak(thing):
if hasattr(thing, 'speak'):
print(thing.speak())
else:
print(f"{type(thing).__name__} cannot speak")
4.7.3. Option 3: Use callable() for Methods
def make_it_speak(thing):
speak_method = getattr(thing, 'speak', None)
if callable(speak_method):
print(speak_method())
else:
print(f"{type(thing).__name__} cannot speak")
|
Note
|
EAFP (try/except) is generally preferred in Python because:
|
4.8. Duck Typing Summary
| Concept | Description |
|---|---|
Core Idea |
Care about behavior, not type |
Benefit |
Flexible, loosely-coupled code |
Risk |
Runtime errors if object lacks expected method |
Best Practice |
Use try/except (EAFP) for graceful handling |
Common Uses |
Iteration, file operations, context managers |
5. Putting It All Together
Let’s build a small example that combines all three concepts:
class DataSource:
"""Base class for data sources using properties and meant for duck typing."""
def __init__(self, name):
self._name = name
self._data = []
@property
def name(self):
return self._name
@property
def record_count(self):
"""Calculated property."""
return len(self._data)
def read(self):
"""Duck typing target: anything with read() can be a data source."""
raise NotImplementedError
class CSVSource(DataSource):
"""Reads data from a CSV-like format."""
def __init__(self, name, raw_text):
super().__init__(name) # Call parent's __init__
self._raw_text = raw_text
def read(self):
"""Parse CSV and return records."""
lines = self._raw_text.strip().split('\n')
headers = lines[0].split(',')
self._data = []
for line in lines[1:]:
values = line.split(',')
record = dict(zip(headers, values))
self._data.append(record)
return self._data
class JSONSource(DataSource):
"""Reads data from JSON format."""
def __init__(self, name, json_data):
super().__init__(name)
self._json_data = json_data
def read(self):
"""Parse JSON and return records."""
import json
self._data = json.loads(self._json_data)
return self._data
def analyze_data(source):
"""
Works with ANY object that has read() and record_count.
This is duck typing in action.
"""
try:
data = source.read()
print(f"Source: {source.name}")
print(f"Records: {source.record_count}")
print(f"First record: {data[0] if data else 'No data'}")
print()
except AttributeError as e:
print(f"Invalid data source: {e}")
# Usage
csv_data = """name,age,city
Alice,30,New York
Bob,25,Boston
Charlie,35,Chicago"""
json_data = '[{"name": "Diana", "score": 95}, {"name": "Eve", "score": 87}]'
csv_source = CSVSource("Employee CSV", csv_data)
json_source = JSONSource("Scores JSON", json_data)
# Both work with the same function thanks to duck typing
analyze_data(csv_source)
# Source: Employee CSV
# Records: 3
# First record: {'name': 'Alice', 'age': '30', 'city': 'New York'}
analyze_data(json_source)
# Source: Scores JSON
# Records: 2
# First record: {'name': 'Diana', 'score': 95}
6. Summary
In this section, you learned:
-
@propertylets you create attributes with built-in logic — validation, calculation, or transformation — while keeping a clean interface -
super()is your tool for building on parent class functionality without repeating yourself. Use it ininitand any other method you want to extend -
Duck typing is Python’s philosophy of caring about what an object can do, not what it is. Write functions that expect behaviors (methods), not specific types
These three concepts will make your Python code more Pythonic, maintainable, and flexible. They’re the difference between code that merely works and code that feels natural to write and read.
7. Practice Exercises
-
Create a
BankAccountclass with abalanceproperty that prevents negative balances. Add a@propertyforis_overdrawnthat returnsTrueif balance is zero. -
Build a
Vehiclebase class withmake,model, andyear. CreateCarandMotorcyclechild classes that usesuper()to initialize the parent and add their own attributes (num_doorsfor Car,has_sidecarfor Motorcycle). -
Write a function
get_length(thing)that uses duck typing to return the length of any object. It should work with strings, lists, dictionaries, and any custom class that implementslen. Handle objects that don’t have a length gracefully. -
Create a
Rectangleclass withwidthandheightproperties that validate positive values. Add calculated properties forareaandperimeter. Include a@propertysetter that allows settingareaby adjusting the width while keeping the aspect ratio. -
Build a simple plugin system using duck typing. Create a
PluginManagerthat accepts any object withactivate()anddeactivate()methods. Test it with different "plugin" classes that don’t share a common parent. # First record: {'name': 'Diana', 'score': 95}
== Summary
In this section, you learned:
* **`@property`** lets you create attributes with built-in logic — validation, calculation, or transformation — while keeping a clean interface
* **`super()`** is your tool for building on parent class functionality without repeating yourself. Use it in `__init__` and any other method you want to extend
* **Duck typing** is Python's philosophy of caring about what an object can _do_, not what it _is_. Write functions that expect behaviors (methods), not specific types
These three concepts will make your Python code more Pythonic, maintainable, and flexible. They're the difference between code that merely works and code that feels natural to write and read.
== Practice Exercises
1. Create a `BankAccount` class with a `balance` property that prevents negative balances. Add a `@property` for `is_overdrawn` that returns `True` if balance is zero.
2. Build a `Vehicle` base class with `make`, `model`, and `year`. Create `Car` and `Motorcycle` child classes that use `super()` to initialize the parent and add their own attributes (`num_doors` for Car, `has_sidecar` for Motorcycle).
3. Write a function `get_length(thing)` that uses duck typing to return the length of any object. It should work with strings, lists, dictionaries, and any custom class that implements `__len__`. Handle objects that don't have a length gracefully.
4. Create a `Rectangle` class with `width` and `height` properties that validate positive values. Add calculated properties for `area` and `perimeter`. Include a `@property` setter that allows setting `area` by adjusting the width while keeping the aspect ratio.
5. Build a simple plugin system using duck typing. Create a `PluginManager` that accepts any object with `activate()` and `deactivate()` methods. Test it with different "plugin" classes that don't share a common parent.
def read(self):
"""Parse CSV and return records."""
lines = self._raw_text.strip().split('\n')
headers = lines[0].split(',')
self._data = []
for line in lines[1:]:
values = line.split(',')
record = dict(zip(headers, values))
self._data.append(record)
return self._data
class JSONSource(DataSource):
"""Reads data from JSON format."""
def __init__(self, name, json_data):
super().__init__(name)
self._json_data = json_data
def read(self):
"""Parse JSON and return records."""
import json
self._data = json.loads(self._json_data)
return self._data
def analyze_data(source):
"""
Works with ANY object that has read() and record_count.
This is duck typing in action.
"""
try:
data = source.read()
print(f"Source: {source.name}")
print(f"Records: {source.record_count}")
print(f"First record: {data[0] if data else 'No data'}")
print()
except AttributeError as e:
print(f"Invalid data source: {e}")
# Usage
csv_data = """name,age,city
Alice,30,New York
Bob,25,Boston
Charlie,35,Chicago"""
json_data = '[{"name": "Diana", "score": 95}, {"name": "Eve", "score": 87}]'
csv_source = CSVSource("Employee CSV", csv_data)
json_source = JSONSource("Scores JSON", json_data)
# Both work with the same function thanks to duck typing
analyze_data(csv_source)
# Source: Employee CSV
# Records: 3
# First record: {'name': 'Alice', 'age': '30', 'city': 'New York'}
analyze_data(json_source)
# Source: Scores JSON
# Records: 2
# First record: {'name': 'Diana', 'score': 95}
8. Summary
In this section, you learned:
-
@propertylets you create attributes with built-in logic — validation, calculation, or transformation — while keeping a clean interface -
super()is your tool for building on parent class functionality without repeating yourself. Use it ininitand any other method you want to extend -
Duck typing is Python’s philosophy of caring about what an object can do, not what it is. Write functions that expect behaviors (methods), not specific types
These three concepts will make your Python code more Pythonic, maintainable, and flexible. They’re the difference between code that merely works and code that feels natural to write and read.
9. Practice Exercises
-
Create a
BankAccountclass with abalanceproperty that prevents negative balances. Add a@propertyforis_overdrawnthat returnsTrueif balance is zero. -
Build a
Vehiclebase class withmake,model, andyear. CreateCarandMotorcyclechild classes that usesuper()to initialize the parent and add their own attributes (num_doorsfor Car,has_sidecarfor Motorcycle). -
Write a function
get_length(thing)that uses duck typing to return the length of any object. It should work with strings, lists, dictionaries, and any custom class that implementslen. Handle objects that don’t have a length gracefully. -
Create a
Rectangleclass withwidthandheightproperties that validate positive values. Add calculated properties forareaandperimeter. Include a@propertysetter that allows settingareaby adjusting the width while keeping the aspect ratio. -
Build a simple plugin system using duck typing. Create a
PluginManagerthat accepts any object withactivate()anddeactivate()methods. Test it with different "plugin" classes that don’t share a common parent.