Chapter 15: Functions, Loops & OOP
Write the same block of code in three places and you have a problem. Change one, forget to change the others, and your program gives three different answers for the same question. The solution is a function — write the logic once, give it a name, and call it wherever you need it.
Loops solve the repetition problem in a different direction. Instead of writing the same code three times for three records, you write it once and let the loop run it across all three — or three thousand.
Object-oriented programming combines both ideas. A class bundles data and the functions that operate on it into one unit. Instead of passing a dictionary around to a dozen separate functions, the dictionary and its functions live together.
This chapter covers all three.
15.1 Functions
A function is a named, reusable block of code. You define it once and call it as many times as you need.
15.1.1 Defining and Calling a Function
def show_status():
print("Pipeline: running")
print("Records loaded: 4500")
show_status()
show_status()
Pipeline: running
Records loaded: 4500
Pipeline: running
Records loaded: 4500
Anatomy of a Function
def calculate_tax ( amount, rate ) :
─── ───────────── ─────────────
│ │ │
keyword name parameters
│
┌─────────────────────┘
▼
result = amount * rate ← function body (indented)
return result ← sends a value back
def is the keyword. The name comes next. Parameters go inside the parentheses. The indented block is the body — everything that runs when the function is called.
15.1.2 Parameters and Arguments
A function that always does the same thing has limited use. Parameters let you pass information in so the function can work with different data each time.
def greet(name):
print(f"Hello, {name}.")
greet("Alice")
greet("Bob")
Hello, Alice.
Hello, Bob.
name is the parameter — a placeholder in the definition. "Alice" and "Bob" are arguments — the actual values passed when calling.
A function can take multiple parameters:
def describe_file(filename, size_mb):
print(f"{filename} — {size_mb} MB")
describe_file("sales_2024.csv", 12.4)
describe_file("logs.txt", 0.8)
sales_2024.csv — 12.4 MB
logs.txt — 0.8 MB
15.1.3 Return Values
return sends a value back to wherever the function was called. That value can be stored, printed, passed to another function, or used in a calculation.
def completion_rate(done, total):
return (done / total) * 100
rate_a = completion_rate(18, 24)
rate_b = completion_rate(9, 15)
print(f"Project A: {rate_a:.1f}%")
print(f"Project B: {rate_b:.1f}%")
Project A: 75.0%
Project B: 60.0%
⚠ Common Mistake — Printing Instead of Returning
def completion_rate(done, total):print((done / total) * 100) # prints but returns nothingresult = completion_rate(18, 24)print(result)75.0NoneThe function printed
75.0, butresultisNone— nothing was returned. A function that only prints cannot feed its result into another calculation. Return the value. Print it separately.
15.1.4 Default Parameters
A parameter can have a default value. If the caller does not pass that argument, Python uses the default.
def apply_tax(amount, rate=0.17):
return amount + (amount * rate)
print(apply_tax(1000)) # uses default rate 0.17
print(apply_tax(1000, 0.10)) # overrides with 0.10
1170.0
1100.0
Default parameters must come after non-default ones in the function signature.
15.1.5 Docstrings
A docstring is a description written as the first line inside a function. It documents what the function does, what it takes, and what it returns.
def completion_rate(done, total):
"""
Calculate task completion as a percentage.
Args:
done (int): Number of completed tasks.
total (int): Total number of tasks.
Returns:
float: Percentage between 0.0 and 100.0.
"""
return (done / total) * 100
Call help(completion_rate) in your shell and Python prints the docstring. In a team, a well-documented function saves hours of guesswork.
Try It 15.1 — Write a function
days_remaining(target, completed)that returns the number of days left, assuming each day completes the same number of tasks as completed so far. Add a docstring. Test it with three different inputs.
15.2 Loops
Loops run a block of code repeatedly. Python has two: for iterates over a sequence, while repeats until a condition becomes False.
15.2.1 The for Loop
cities = ["Karachi", "Lahore", "Islamabad", "Peshawar"]
for city in cities:
print(f"Processing data for: {city}")
Processing data for: Karachi
Processing data for: Lahore
Processing data for: Islamabad
Processing data for: Peshawar
How a for Loop Iterates
cities = ["Karachi", "Lahore", "Islamabad", "Peshawar"]
─────── ────── ───────── ────────
▲ ▲ ▲ ▲
step 1 step 2 step 3 step 4
for city in cities:
print(city)
city = "Karachi" → runs body
city = "Lahore" → runs body
city = "Islamabad" → runs body
city = "Peshawar" → runs body → loop ends
The loop variable (city) takes each value from the list, one at a time. You choose the name — it can be anything.
15.2.2 range()
range() generates a sequence of numbers on demand, without building a list.
for i in range(4):
print(f"Batch {i + 1} of 4 complete.")
Batch 1 of 4 complete.
Batch 2 of 4 complete.
Batch 3 of 4 complete.
Batch 4 of 4 complete.
range() Variations
| Call | Produces |
|---|---|
range(5) | 0, 1, 2, 3, 4 |
range(1, 6) | 1, 2, 3, 4, 5 |
range(0, 10, 2) | 0, 2, 4, 6, 8 |
range(10, 0, -1) | 10, 9, 8 ... 1 |
15.2.3 Looping Over Dictionaries
In Chapter 14 you saw .keys(), .values(), and .items(). Here is where they become genuinely useful — combining them with a for loop.
server = {
"host": "db-01",
"port": 5432,
"region": "us-east-1",
"status": "online"
}
# Loop through all keys
for key in server.keys():
print(key)
host
port
region
status
# Loop through all values
for value in server.values():
print(value)
db-01
5432
us-east-1
online
# Loop through key-value pairs — the most useful pattern
for key, value in server.items():
print(f"{key}: {value}")
host: db-01
port: 5432
region: us-east-1
status: online
.items() unpacks each pair into two variables. You will use this constantly when printing records, building reports, or transforming data.
You can also loop over a list of dictionaries — the most common data structure in real pipelines:
records = [
{"filename": "sales_jan.csv", "rows": 4200, "status": "loaded"},
{"filename": "sales_feb.csv", "rows": 3850, "status": "failed"},
{"filename": "sales_mar.csv", "rows": 4900, "status": "loaded"},
]
for r in records:
print(f"{r['filename']} — {r['rows']} rows — {r['status'].upper()}")
sales_jan.csv — 4200 rows — LOADED
sales_feb.csv — 3850 rows — FAILED
sales_mar.csv — 4900 rows — LOADED
15.2.4 The while Loop
A while loop keeps running as long as its condition is True. Use it when you do not know in advance how many iterations you need.
attempts = 0
max_attempts = 3
connected = False
while not connected and attempts < max_attempts:
attempts += 1
print(f"Connection attempt {attempts}...")
if attempts == 2:
connected = True
print("Connected." if connected else "Failed after max attempts.")
Connection attempt 1...
Connection attempt 2...
Connected.
for vs while — When to Use Each
for | while | |
|---|---|---|
| Use when | You know how many iterations | You don't know — stop on a condition |
| Common use | Lists, dicts, ranges | Retries, polling, user input |
| Risk | Almost none | Infinite loop if condition never changes |
⚠ Common Mistake — Infinite Loop A
whileloop whose condition never becomesFalseruns forever:count = 0while count < 5:print(count)# forgot count += 1 — loops foreverAlways make sure something inside the loop moves the condition toward
False.
15.2.5 break and continue
break exits the loop immediately. continue skips the rest of the current iteration and moves to the next.
scores = [88, 72, 45, 91, 55, 60]
for score in scores:
if score < 50:
print(f"Score {score} is too low — stopping review.")
break
if score < 60:
continue # skip scores below 60, don't print them
print(f"Valid score: {score}")
Valid score: 88
Valid score: 72
Score 45 is too low — stopping review.
What break and continue Do
Loop iteration:
┌──────────────────────────┐
│ run body │
│ hit continue ──────────▶│ skip rest, go to next item
│ hit break ──────────────────────────▶ exit loop entirely
└──────────────────────────┘
Try It 15.2 — Write a loop over a list of six file sizes in MB. Skip any file under 1 MB using
continue. Stop the loop if a file exceeds 500 MB usingbreak. Print each file size you do process.
15.3 Object-Oriented Programming
A dictionary holds data. A function operates on data. But once you have many functions all working on the same kind of dictionary, you start to wish they lived together. That is what a class provides — data and the functions that operate on it, in one place.
15.3.1 What Is a Class?
A class is a blueprint. It describes what a thing holds (its attributes) and what it can do (its methods). An instance is one specific thing built from that blueprint.
CLASS (blueprint)
┌───────────────────┐
│ BankAccount │
│ ───────────── │
│ Attributes: │
│ · owner │
│ · balance │
│ │
│ Methods: │
│ · deposit() │
│ · withdraw() │
│ · summary() │
└─────────┬─────────┘
│ build from blueprint
┌──────────┴──────────┐
▼ ▼
alice_account bob_account
owner = "Alice" owner = "Bob"
balance = 5000 balance = 1200
Every instance shares the same blueprint but holds its own independent data.
15.3.2 Defining a Class
class BankAccount:
"""A simple bank account."""
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
def deposit(self, amount):
self.balance += amount
def withdraw(self, amount):
if amount > self.balance:
return "Insufficient funds."
self.balance -= amount
return f"Withdrawn: {amount}"
def summary(self):
return f"{self.owner}: ${self.balance:,.2f}"
Class Anatomy
class BankAccount:
─────
│
└── class keyword + name
def __init__(self, owner, balance=0):
────────
│
└── constructor — runs when object is created
self = "this specific instance"
self.owner = owner ← instance attribute
self.balance = balance ← instance attribute
def deposit(self, amount): ← method (function inside class)
self.balance += amount
__init__runs automatically when you create an object. It sets up the starting state.selfis how the method refers to the specific instance it belongs to. Every method must have it as its first parameter.self.ownerandself.balanceare instance attributes — each object gets its own copy.
15.3.3 Creating and Using Instances
alice = BankAccount("Alice", 5000)
bob = BankAccount("Bob") # balance defaults to 0
alice.deposit(1500)
bob.deposit(800)
print(alice.withdraw(200))
print(alice.summary())
print(bob.summary())
Withdrawn: 200
Alice: $6,300.00
Bob: $800.00
alice and bob are independent objects. A deposit into alice does not affect bob. They share the same blueprint, but each holds its own data.
⚠ Common Mistake — Forgetting
selfEvery method needsselfas its first parameter, and every attribute access needsself.as a prefix:def summary(): # TypeError — self is missingdef summary(self): # Correctdef summary(self):return owner # NameError — Python doesn't know which 'owner'def summary(self):return self.owner # Correct
Try It 15.3 — Create a
Productclass withname,price, andin_stock(defaultTrue). Add a methodapply_discount(percent)that reduces the price, and a methodinfo()that returns a formatted string with all three fields. Create two products and test both methods.
15.4 The Four Pillars of OOP
Every object-oriented language is built on four principles. Python supports all of them.
┌─────────────────┬──────────────────┬──────────────────┬─────────────────┐
│ Encapsulation │ Inheritance │ Polymorphism │ Abstraction │
│ │ │ │ │
│ Bundle data and │ A child class │ Same method name │ Hide complexity │
│ behavior. Control│ inherits from │ different │ behind a simple │
│ who can access │ a parent. Reuse │ behavior in each │ interface. │
│ what. │ and extend. │ class. │ │
└─────────────────┴──────────────────┴──────────────────┴─────────────────┘
15.4.1 Encapsulation
Encapsulation bundles data and the methods that work on it into one class, and controls how that data is accessed or changed from outside.
Python has three levels of access for attributes and methods:
| Level | Syntax | Accessible from | Convention |
|---|---|---|---|
| Public | name | Anywhere | Default — use freely |
| Protected | _name | Class and subclasses | Internal — avoid accessing from outside |
| Private | __name | Class only (name mangled) | Strictly internal — enforced by Python |
class Person:
def __init__(self, name, age, password):
self.name = name # public — anyone can read or change
self._age = age # protected — intended for internal use
self.__password = password # private — name mangled by Python
def get_age(self):
return self._age # controlled access through a method
def check_password(self, attempt):
return attempt == self.__password
p = Person("Alice", 30, "secret123")
print(p.name) # ✓ Public — works fine
print(p._age) # ✓ Works, but you shouldn't do this outside the class
print(p.get_age()) # ✓ Correct way to access protected data
print(p.check_password("secret123"))
Alice
30
30
True
Now try accessing the private attribute directly:
print(p.__password)
AttributeError: 'Person' object has no attribute '__password'
Python renamed it internally. You can still reach it with the mangled name — but this is a signal that you are breaking the rules:
print(p._Person__password) # name mangling — works but do not do this
secret123
Public vs Protected vs Private — When to Use Each
Public (name)
└── For data that is genuinely meant to be shared — name, title, status
Protected (_name)
└── For internal data that subclasses might need — use a method for outsiders
Private (__name)
└── For sensitive data that must never be touched from outside — passwords,
internal counters, financial figures
The underscore is a convention in Python, not a hard lock (except __ which applies name mangling). The message it sends is: "this is not part of the public interface — if you reach in here, you are on your own."
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner # public
self._balance = balance # protected
self.__pin = "0000" # private
def deposit(self, amount):
if amount > 0:
self._balance += amount
def get_balance(self):
return self._balance # read-only access through a method
def change_pin(self, old, new):
if old == self.__pin:
self.__pin = new
return "PIN updated."
return "Incorrect PIN."
acc = BankAccount("Bob", 1000)
acc.deposit(500)
print(acc.get_balance())
print(acc.change_pin("0000", "1234"))
1500
PIN updated.
_balance can be read via get_balance(). __pin can only be changed through change_pin(), which validates the old PIN first. No outside code can skip that check.
Try It 15.4 — Create an
Employeeclass with a publicname, a protected_salary, and a private__employee_id. Add a methodget_id()that returns the last four digits of the ID only. Try accessing__employee_iddirectly from outside and observe the error.
15.4.2 Inheritance
Inheritance lets a child class take on all the attributes and methods of a parent class, then add or change what it needs. Python supports five types.
Type 1 — Single Inheritance
One parent, one child. The simplest form.
Animal
│
Dog
class Animal:
def __init__(self, name):
self.name = name
def eat(self):
return f"{self.name} is eating."
def speak(self):
return f"{self.name} makes a sound."
class Dog(Animal):
def speak(self): # overrides the parent method
return f"{self.name} says: Woof!"
dog = Dog("Bruno")
print(dog.eat()) # inherited from Animal
print(dog.speak()) # overridden in Dog
Bruno is eating.
Bruno says: Woof!
Dog inherited eat() without rewriting it. It replaced speak() with its own version.
Type 2 — Multilevel Inheritance
A chain: grandparent → parent → child.
Animal
│
Mammal
│
Dog
class Animal:
def breathe(self):
return "Breathing air."
class Mammal(Animal):
def feed_young(self):
return "Feeding young with milk."
class Dog(Mammal):
def speak(self):
return "Woof!"
dog = Dog()
print(dog.breathe()) # from Animal
print(dog.feed_young()) # from Mammal
print(dog.speak()) # from Dog
Breathing air.
Feeding young with milk.
Woof!
Dog has access to every method from every level above it in the chain.
Type 3 — Hierarchical Inheritance
One parent, multiple children. Each child extends the parent differently.
Animal
/ | \
Dog Cat Bird
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} makes a sound."
class Dog(Animal):
def speak(self):
return f"{self.name} says: Woof!"
class Cat(Animal):
def speak(self):
return f"{self.name} says: Meow!"
class Bird(Animal):
def speak(self):
return f"{self.name} says: Tweet!"
animals = [Dog("Bruno"), Cat("Whiskers"), Bird("Tweety")]
for animal in animals:
print(animal.speak())
Bruno says: Woof!
Whiskers says: Meow!
Tweety says: Tweet!
Type 4 — Multiple Inheritance
A child inherits from more than one parent.
Animal Swimmer
\ /
Duck
class Animal:
def __init__(self, name):
self.name = name
def breathe(self):
return f"{self.name} breathes air."
class Swimmer:
def swim(self):
return "Swimming through water."
class Duck(Animal, Swimmer): # inherits from both
def speak(self):
return f"{self.name} says: Quack!"
duck = Duck("Donald")
print(duck.breathe()) # from Animal
print(duck.swim()) # from Swimmer
print(duck.speak()) # Duck's own method
Donald breathes air.
Swimming through water.
Donald says: Quack!
Python resolves which parent's method to use through the Method Resolution Order (MRO) — left to right, then up the chain. Check it with Duck.__mro__.
Type 5 — Hybrid Inheritance
A combination of two or more of the above types. For example, hierarchical inheritance where one of the children also has multiple parents.
Animal
/ \
Mammal Swimmer
\ /
Dolphin
class Animal:
def breathe(self):
return "Breathing."
class Mammal(Animal):
def feed_young(self):
return "Feeding young."
class Swimmer(Animal):
def swim(self):
return "Swimming."
class Dolphin(Mammal, Swimmer):
def speak(self):
return "Click click!"
d = Dolphin()
print(d.breathe()) # Animal
print(d.feed_young()) # Mammal
print(d.swim()) # Swimmer
print(d.speak()) # Dolphin
Breathing.
Feeding young.
Swimming.
Click click!
Inheritance Types at a Glance
| Type | Structure | Example |
|---|---|---|
| Single | One parent → one child | Animal → Dog |
| Multilevel | Grandparent → parent → child | Animal → Mammal → Dog |
| Hierarchical | One parent → many children | Animal → Dog, Cat, Bird |
| Multiple | Two parents → one child | Animal + Swimmer → Duck |
| Hybrid | Combination of the above | Mammal + Swimmer → Dolphin |
Try It 15.5 — Build a three-level multilevel chain:
Vehicle→Car→ElectricCar. Each level adds one attribute and one method. Create anElectricCarobject and call a method from each level of the chain.
15.4.3 Polymorphism
Polymorphism means the same method name produces different behavior depending on which class is calling it. The code that uses the objects stays the same — only the output changes.
Example 1 — Animals Speaking
The most natural example. Every animal has a speak() method. Every animal speaks differently.
class Dog:
def speak(self):
return "Woof!"
class Cat:
def speak(self):
return "Meow!"
class Bird:
def speak(self):
return "Tweet!"
class Cow:
def speak(self):
return "Moo!"
animals = [Dog(), Cat(), Bird(), Cow()]
for animal in animals:
print(f"{animal.__class__.__name__}: {animal.speak()}")
Dog: Woof!
Cat: Meow!
Bird: Tweet!
Cow: Moo!
The loop is identical. Python decides which speak() to call based on the actual type of each object. Add a Lion class with its own speak() — the loop needs zero changes.
Example 2 — Shapes Calculating Area
Different shapes share the same method name but compute it completely differently.
import math
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def describe(self):
return f"Rectangle {self.width}×{self.height}"
class Circle:
def __init__(self, radius):
self.radius = radius
def area(self):
return math.pi * self.radius ** 2
def describe(self):
return f"Circle r={self.radius}"
class Triangle:
def __init__(self, base, height):
self.base = base
self.height = height
def area(self):
return 0.5 * self.base * self.height
def describe(self):
return f"Triangle base={self.base} height={self.height}"
shapes = [Rectangle(4, 6), Circle(5), Triangle(8, 3)]
for shape in shapes:
print(f"{shape.describe()} → area = {shape.area():.2f}")
Rectangle 4×6 → area = 24.00
Circle r=5 → area = 78.54
Triangle base=8 height=3 → area = 12.00
Same call: shape.area()
│
┌────────────┼────────────┐
▼ ▼ ▼
Rectangle Circle Triangle
width×height π×r² ½×b×h
The key point in both examples: the calling code treats all objects identically. Each class decides for itself what speak() or area() means.
Try It 15.6 — Add a
Squareclass to the shapes example above. It should have a singlesideattribute and implementarea()anddescribe(). Add it to theshapeslist — confirm the loop prints the correct result without changing any other line.
15.4.4 Abstraction
Abstraction means showing only what the user of a class needs to see, and hiding how it actually works inside. The interface is simple. The complexity is invisible.
Think of a TV remote. You press Volume Up. You do not see the infrared signal, the receiver circuit, the firmware decoding it. The button is the abstraction — a simple interface over complex machinery.
Python's abc module (Abstract Base Classes) lets you define a formal interface — a class with methods that must be implemented by any child class.
from abc import ABC, abstractmethod
A class that inherits from ABC and marks methods with @abstractmethod cannot be instantiated directly. It forces every child to implement those methods.
Example 1 — Remote Control
from abc import ABC, abstractmethod
class Remote(ABC):
"""Abstract base: every remote must have these controls."""
@abstractmethod
def volume_up(self):
pass
@abstractmethod
def volume_down(self):
pass
@abstractmethod
def power(self):
pass
class TVRemote(Remote):
def volume_up(self):
return "TV volume: increased"
def volume_down(self):
return "TV volume: decreased"
def power(self):
return "TV: toggled on/off"
class ACRemote(Remote):
def volume_up(self):
return "AC temperature: increased"
def volume_down(self):
return "AC temperature: decreased"
def power(self):
return "AC: toggled on/off"
tv = TVRemote()
ac = ACRemote()
print(tv.power())
print(ac.volume_up())
TV: toggled on/off
AC temperature: increased
Try to instantiate Remote directly:
r = Remote()
TypeError: Can't instantiate abstract class Remote with abstract methods power, volume_down, volume_up
The abstract class enforces the contract. Any class that inherits from Remote must implement all three methods — or Python refuses to create objects from it.
Example 2 — Vehicle
from abc import ABC, abstractmethod
class Vehicle(ABC):
def __init__(self, brand):
self.brand = brand
@abstractmethod
def fuel_type(self):
pass
@abstractmethod
def start_engine(self):
pass
def describe(self): # concrete method — shared by all
return f"{self.brand} — {self.fuel_type()}"
class Car(Vehicle):
def fuel_type(self):
return "Petrol"
def start_engine(self):
return f"{self.brand}: Vroom!"
class ElectricCar(Vehicle):
def fuel_type(self):
return "Electric"
def start_engine(self):
return f"{self.brand}: Whirr..."
class Bicycle(Vehicle):
def fuel_type(self):
return "Human-powered"
def start_engine(self):
return f"{self.brand}: Pedalling!"
vehicles = [Car("Toyota"), ElectricCar("Tesla"), Bicycle("Trek")]
for v in vehicles:
print(v.describe())
print(v.start_engine())
print()
Toyota — Petrol
Toyota: Vroom!
Tesla — Electric
Tesla: Whirr...
Trek — Human-powered
Trek: Pedalling!
describe() is a concrete method defined once in the abstract class — all children get it for free. fuel_type() and start_engine() are abstract — every child must define its own version.
The Four Pillars — Summary Table
| Pillar | What it does | Python mechanism | Real-world analogy |
|---|---|---|---|
| Encapsulation | Protects internal state | _attr, __attr, methods as gatekeepers | A vending machine — you press buttons, you don't touch the motor |
| Inheritance | Reuses and extends code | class Child(Parent), super() | A child inherits traits from a parent |
| Polymorphism | One interface, many behaviors | Same method name, different implementation | A speak() command: dog barks, cat meows |
| Abstraction | Hides complexity | ABC, @abstractmethod | A TV remote — buttons hide the electronics |
15.5 Putting It Together
Here is a complete example using functions, loops, and a class together. It processes a batch of uploaded files and flags any that failed.
class UploadedFile:
"""Represents a single file in a data upload batch."""
def __init__(self, name, size_mb, status):
self.name = name
self.size_mb = size_mb
self.status = status
def is_valid(self):
return self.status == "success" and self.size_mb > 0
def report(self):
flag = "✓" if self.is_valid() else "✗"
return f"{flag} {self.name} ({self.size_mb} MB) — {self.status}"
def count_failed(files):
"""Return the number of files that did not load successfully."""
failed = 0
for f in files:
if not f.is_valid():
failed += 1
return failed
# Batch of uploaded files — Ayan's upload from yesterday
files = [
UploadedFile("customers.csv", 14.2, "success"),
UploadedFile("orders.csv", 0.0, "empty"),
UploadedFile("products.csv", 8.7, "success"),
UploadedFile("returns.csv", 3.1, "failed"),
UploadedFile("inventory.csv", 5.5, "success"),
]
print("=== UPLOAD BATCH REPORT ===")
for f in files:
print(f.report())
total = len(files)
failed = count_failed(files)
success = total - failed
print(f"\nTotal: {total} | Success: {success} | Failed: {failed}")
=== UPLOAD BATCH REPORT ===
✓ customers.csv (14.2 MB) — success
✗ orders.csv (0.0 MB) — empty
✓ products.csv (8.7 MB) — success
✗ returns.csv (3.1 MB) — failed
✓ inventory.csv (5.5 MB) — success
Total: 5 | Success: 3 | Failed: 2
The class holds the data and the logic about a single file. The function works across a collection. The loop runs both over every item in the batch.
Summary
A function is defined with def, receives data through parameters, and sends results back with return. Default parameters make arguments optional. Docstrings document the function's contract. The for loop iterates over any sequence — a list, a dictionary's .items(), or a range(). The while loop repeats until a condition becomes False. break exits a loop; continue skips the current iteration. A class is a blueprint: __init__ sets up the object, self refers to the specific instance, attributes hold data, and methods define behavior. OOP rests on four pillars. Encapsulation controls access through public (name), protected (_name), and private (__name) attributes — private attributes are name-mangled by Python. Inheritance comes in five types: single, multilevel, hierarchical, multiple, and hybrid — all use class Child(Parent) and super(). Polymorphism lets different classes share a method name but behave differently — the calling code stays identical. Abstraction enforces a contract through ABC and @abstractmethod, hiding implementation and exposing only the interface.
Exercises
15.1 — Write a function file_summary(name, size_mb, status) that returns a single formatted string: "orders.csv | 8.4 MB | STATUS". Make the status always uppercase inside the function. Test it with three different inputs.
15.2 — You have a list of dictionaries, each with "month" and "revenue" keys. Write a loop using .items() that prints each key-value pair. Then write a second loop that prints only months where revenue exceeded 50,000.
15.3 — The following function has two bugs. Find them and explain what each one causes:
def summarise_files(files, min_size):
valid = []
for file in files
if file["size_mb"] > min_size:
valid.append(file["name"])
return valid
15.4 — Create a Server class with attributes hostname, region, and active (default True). Add a method deactivate() that sets active to False, and a method status() that returns "online" or "offline". Create three server instances, deactivate one, and print each server's status.
15.5 — Extend the BankAccount class from section 15.3.2 with a transaction_history list (initialised empty in __init__). Every time deposit() or withdraw() is called, append a string describing the transaction. Add a method print_history() that prints each entry numbered.
15.6 — Create an Employee class with a public name, a protected _department, and a private __salary. Add a method get_salary_band() that returns "Junior", "Mid", or "Senior" based on salary ranges — without exposing the actual number. Try accessing __salary directly from outside the class and explain exactly what Python does and why.
15.7 — Build a three-level multilevel chain: LivingThing → Animal → Dog. LivingThing has breathe(). Animal adds eat() and speak(). Dog overrides speak() to return "Woof!" and adds fetch(). Create a Dog instance and call all four methods — label which class each method comes from.
15.8 — Create four classes: Shape (parent with area() raising NotImplementedError), Circle, Rectangle, and Triangle. Loop through a mixed list of all three child types calling .area() on each. Do not check the type of any object inside the loop.
15.9 — Using ABC and @abstractmethod, create an abstract class Appliance with abstract methods turn_on() and turn_off(). Build two concrete classes: WashingMachine and AirConditioner. Demonstrate that trying to instantiate Appliance directly raises a TypeError, then create one instance of each child class and call both methods.
15.10 — In section 15.4.1, __pin is stored by Python under a mangled name. Write the exact name Python uses to store it. Then explain the difference between what _name (single underscore) and __name (double underscore) actually enforce — one is a convention, the other changes behaviour. Which is which, and what does each actually prevent?