Module 1 · Python · Drills
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.
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.
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").
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.
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.
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.
# --- 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},
]
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
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).
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.
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.
# 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.
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__.
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.
Click a card to flip it. Say the answer out loud before you flip — that's the rep that builds storage strength.
self?__init__ for?*args vs **kwargs?*args collects extra positional args into a tuple; **kwargs collects extra keyword args into a dict.@dataclass do?__init__, __repr__ and equality from typed fields. The mental model for Pydantic.python -m venv .venv then source .venv/bin/activate (Windows: .venv\Scripts\activate).if __name__ == "__main__":?Tick each only if you can do it without looking:
*args and **kwargs correctly__init__, methods, and a useful __repr__@dataclass__main__ guardrequirements.txtDocumentStore and understand the class-wrapping-a-list pattern