Module 1 · Python · Drills

Drills: Functions, OOP & Modules

Reading is not knowing. Type every one of these yourself — in the REPL or a file — before you reveal the solution. Effortful recall is the point.

How to use this page Each drill is a small task. Attempt it first, run it, then click “Show solution” to compare. If yours works differently but correctly — great, that's fluency. Tick each box in the self-check as you go; your progress is saved in this browser.

A · Warm-up reps Basic

Drill 1 defaults

Write greet(name, greeting="Hello") that returns "<greeting>, <name>". Call it once with the default and once overriding it with the keyword.

Show solution
def greet(name, greeting="Hello"):
    return f"{greeting}, {name}"

print(greet("Sam"))                  # Hello, Sam
print(greet("Sam", greeting="Marhaba"))  # Marhaba, Sam

Drill 2 *args / **kwargs

Write describe(*args, **kwargs) that prints the positional arguments as a tuple and the keyword arguments as a dict. Call it with describe(1, 2, user="sam", level="info").

Show solution
def describe(*args, **kwargs):
    print("positional:", args)
    print("keyword:", kwargs)

describe(1, 2, user="sam", level="info")
# positional: (1, 2)
# keyword: {'user': 'sam', 'level': 'info'}

Drill 3 type hints

Write repeat that takes a str and an int (default 2) and returns the string repeated that many times. Annotate both parameters and the return type.

Show solution
def repeat(text: str, times: int = 2) -> str:
    return text * times

print(repeat("ab"))      # abab
print(repeat("x", 3))    # xxx

Remember: the hints don't enforce anything at runtime — they're for tools and readers.

B · The famous trap Intermediate

Drill 4 mutable default bug

First, reproduce the bug: write add_tag(tag, tags=[]) that appends and returns. Call it twice and observe the leak. Then write the corrected version using a None sentinel.

Show solution
# --- the bug ---
def add_tag(tag, tags=[]):
    tags.append(tag)
    return tags

print(add_tag("ai"))    # ['ai']
print(add_tag("uae"))   # ['ai', 'uae']  ← leaked!

# --- the fix ---
def add_tag(tag, tags=None):
    if tags is None:
        tags = []
    tags.append(tag)
    return tags

print(add_tag("ai"))    # ['ai']
print(add_tag("uae"))   # ['uae']  ← independent, correct

The default is evaluated once at definition time, so a list default is shared by every call. None + a fresh list inside fixes it. Be ready to explain this in an interview.

Drill 5 sorted + lambda

Given the list below, sort the documents by pages, largest first, using sorted with a key=lambda.

docs = [
    {"title": "B", "pages": 5},
    {"title": "A", "pages": 9},
    {"title": "C", "pages": 2},
]
Show solution
ordered = sorted(docs, key=lambda d: d["pages"], reverse=True)
for d in ordered:
    print(d["title"], d["pages"])
# A 9 / B 5 / C 2

C · Objects & modules Intermediate

Drill 6 class + __repr__

Write a Document class with __init__(self, title, body), a word_count() method, and a __repr__ that prints Document(title='Intro', words=3).

Show solution
class Document:
    def __init__(self, title, body):
        self.title = title
        self.body = body

    def word_count(self):
        return len(self.body.split())

    def __repr__(self):
        return f"Document(title={self.title!r}, words={self.word_count()})"

print(Document("Intro", "the cat sat"))
# Document(title='Intro', words=3)

The !r calls repr() on the title, which is why it comes out quoted.

Drill 7 @dataclass

Convert the Document from Drill 6 into a @dataclass with typed fields title and body, plus a word_count() method. Confirm printing it gives an auto-generated repr.

Show solution
from dataclasses import dataclass

@dataclass
class Document:
    title: str
    body: str

    def word_count(self) -> int:
        return len(self.body.split())

doc = Document("Intro", "the cat sat")
print(doc)               # Document(title='Intro', body='the cat sat')
print(doc.word_count())  # 3

Notice you wrote no __init__ and no __repr__ — the decorator generated both. This is your mental model for Pydantic next module.

Drill 8 module + __main__

Split your code into a module. Put word_count(text) in textutils.py, then in main.py import it and use it — guarded so a demo only runs when main.py is executed directly.

Show solution
# textutils.py
def word_count(text: str) -> int:
    return len(text.split())
# main.py
from textutils import word_count

def main():
    print(word_count("the cat sat"))   # 3

if __name__ == "__main__":
    main()

Run python main.py → it prints 3. Importing main elsewhere does not trigger the demo, thanks to the guard.

D · Build challenge Build

Mini-project Write a DocumentStore class: it holds a list of Document objects, can add() a document, report total_words() across all docs, and find(keyword) the documents whose body contains a keyword. This is a direct miniature of DocChat's in-memory store.

Build · document store

Start from a simple Document dataclass, then build the store around it. Aim for clean methods and a useful __repr__.

Show solution
from dataclasses import dataclass, field


@dataclass
class Document:
    title: str
    body: str

    def word_count(self) -> int:
        return len(self.body.split())


class DocumentStore:
    def __init__(self):
        self.docs: list[Document] = []

    def add(self, doc: Document) -> None:
        self.docs.append(doc)

    def total_words(self) -> int:
        return sum(d.word_count() for d in self.docs)

    def find(self, keyword: str) -> list[Document]:
        kw = keyword.lower()
        return [d for d in self.docs if kw in d.body.lower()]

    def __repr__(self) -> str:
        return f"DocumentStore({len(self.docs)} docs, {self.total_words()} words)"


def main():
    store = DocumentStore()
    store.add(Document("Intro", "the cat sat on the mat"))
    store.add(Document("Report", "sales in the UAE grew fast"))

    print(store)                       # DocumentStore(2 docs, 12 words)
    print(store.total_words())         # 12
    for d in store.find("uae"):
        print("match:", d.title)        # match: Report


if __name__ == "__main__":
    main()

Note find uses a list comprehension and total_words a generator expression with sum — you'll meet both properly in Lesson 1.3. The structure (a class wrapping a list of records, with query methods) is exactly how DocChat's store begins.

E · Rapid recall Flashcards

Click a card to flip it. Say the answer out loud before you flip — that's the rep that builds storage strength.

What is self?
The current instance, passed automatically as the first parameter of every method.
click to flip
What is __init__ for?
The constructor — it runs when you create an instance and sets up its attributes.
click to flip
*args vs **kwargs?
*args collects extra positional args into a tuple; **kwargs collects extra keyword args into a dict.
click to flip
What does @dataclass do?
Auto-generates __init__, __repr__ and equality from typed fields. The mental model for Pydantic.
click to flip
Create & activate a venv?
python -m venv .venv then source .venv/bin/activate (Windows: .venv\Scripts\activate).
click to flip
What is if __name__ == "__main__":?
A guard that runs the block only when the file is executed directly, not when imported.
click to flip

F · Self-check before moving on

Tick each only if you can do it without looking:

Next All ticked? You can now write reusable, organised Python. Next we make it Pythonic — comprehensions, generators, decorators, and the idioms interviewers listen for: Lesson 1.3 — Pythonic & Advanced.