Technology  /  Pair Programming

๐Ÿ‘ฅ Pair Programming 2 guides ยท updated 2026

Driver-navigator collaboration, the concepts behind it, and hands-on Python exercises โ€” the habit that levels up engineers faster than solo practice.

Python Quick Reference: 30 Patterns Every Developer Should Have at Their Fingertips

This isnโ€™t an introduction to Python. Itโ€™s a working reference โ€” the kind of document youโ€™d want open during a pairing session or before a technical interview. Each section shows a clean, usable pattern with a brief note on whatโ€™s worth knowing about it beyond the obvious.


1. Variables and Data Types

username = "alex" # str
score = 142 # int
temperature = 36.6 # float
is_verified = True # bool
missing = None # NoneType
print(type(username)) # <class 'str'>

Worth knowing: Python is dynamically typed but not loosely typed โ€” "5" + 5 raises a TypeError. Use isinstance() when you need to check types at runtime.


2. Conditional Statements

speed = 85
if speed > 100:
print("Slow down")
elif speed > 60:
print("Cruising speed")
else:
print("Below limit")

Worth knowing: Python has no switch statement pre-3.10. The match statement arrived in Python 3.10 and handles structural pattern matching โ€” worth learning if youโ€™re on a modern version.


3. Loops โ€” for and while

# for loop with enumerate (use this instead of range(len(x)))
fruits = ["apple", "banana", "cherry"]
for index, fruit in enumerate(fruits):
print(index, fruit)
# while loop with break
count = 0
while True:
if count >= 3:
break
print(f"Count: {count}")
count += 1

Worth knowing: Prefer enumerate() over range(len(list)). Use zip() to iterate two lists in parallel. Both are more Pythonic and more readable in code review.


4. Functions and Arguments

def send_email(to, subject, body="No content", cc=None):
cc = cc or []
print(f"Sending to {to}, CC: {cc}")
print(f"Subject: {subject}")
send_email("alice@example.com", "Meeting", cc=["bob@example.com"])

Worth knowing: Default mutable arguments (like def f(x=[])) is a classic Python trap โ€” the list is shared across all calls. Use None as the default and initialise inside the function.


5. List Comprehensions

# Standard
squares = [n ** 2 for n in range(1, 6)]
# With condition
even_squares = [n ** 2 for n in range(1, 11) if n % 2 == 0]
# Nested (use sparingly โ€” readability drops fast)
matrix = [[row * col for col in range(1, 4)] for row in range(1, 4)]
print(squares) # [1, 4, 9, 16, 25]
print(even_squares) # [4, 16, 36, 64, 100]

Worth knowing: List comprehensions are typically faster than equivalent for loops because theyโ€™re optimised at the bytecode level. But if the logic requires more than one condition or a nested structure, a regular loop with a comment is usually clearer.


6. Dictionary Comprehensions

words = ["python", "data", "engineer"]
word_lengths = {word: len(word) for word in words}
print(word_lengths) # {'python': 6, 'data': 4, 'engineer': 8}
# Invert a dictionary
original = {"a": 1, "b": 2, "c": 3}
inverted = {v: k for k, v in original.items()}
print(inverted) # {1: 'a', 2: 'b', 3: 'c'}

7. String Formatting and Manipulation

name = "Python"
version = 3.12
# f-strings (preferred)
print(f"Welcome to {name} {version}")
# Useful string methods
text = " hello world "
print(text.strip()) # "hello world"
print(text.strip().title()) # "Hello World"
print("hello".replace("l", "L")) # "heLLo"
print(",".join(["a", "b", "c"])) # "a,b,c"
print("a,b,c".split(",")) # ['a', 'b', 'c']

Worth knowing: f-strings (available since Python 3.6) are the clearest and fastest string formatting option. Use them by default. .format() is still useful for templates; % formatting is legacy โ€” avoid it in new code.


8. Error Handling with try / except

def safe_divide(a, b):
try:
result = a / b
except ZeroDivisionError:
return None
except TypeError as e:
print(f"Type error: {e}")
return None
else:
return result # runs only if no exception
finally:
print("Done") # always runs
print(safe_divide(10, 2)) # Done / 5.0
print(safe_divide(10, 0)) # Done / None

Worth knowing: Catch specific exceptions, not bare except:. Catching everything hides bugs. The else block on a try is underused โ€” it runs only when no exception occurred, which is semantically cleaner than putting that logic inside the try.


9. Custom Exceptions

class ValidationError(ValueError):
"""Raised when input fails validation rules."""
def __init__(self, field, message):
self.field = field
super().__init__(f"{field}: {message}")
def validate_age(age: int):
if not isinstance(age, int):
raise ValidationError("age", "must be an integer")
if age < 0 or age > 130:
raise ValidationError("age", f"{age} is out of valid range")
return age
try:
validate_age(-5)
except ValidationError as e:
print(e) # age: -5 is out of valid range

10. File Handling

# Writing
with open("output.txt", "w", encoding="utf-8") as f:
f.write("First line\n")
f.writelines(["Second\n", "Third\n"])
# Reading all at once
with open("output.txt", "r", encoding="utf-8") as f:
content = f.read()
# Reading line by line (memory-efficient for large files)
with open("output.txt", "r", encoding="utf-8") as f:
for line in f:
print(line.strip())

Worth knowing: Always use with for file operations โ€” it guarantees the file is closed even if an exception is raised. Always specify encoding="utf-8" explicitly; the default varies by OS and creates subtle bugs.


11. Object-Oriented Programming

class BankAccount:
def __init__(self, owner: str, balance: float = 0):
self.owner = owner
self._balance = balance # _ prefix = convention for "private"
def deposit(self, amount: float):
if amount <= 0:
raise ValueError("Deposit must be positive")
self._balance += amount
def withdraw(self, amount: float):
if amount > self._balance:
raise ValueError("Insufficient funds")
self._balance -= amount
@property
def balance(self):
return self._balance
def __repr__(self):
return f"BankAccount(owner={self.owner!r}, balance={self._balance})"
account = BankAccount("Alice", 100)
account.deposit(50)
print(account.balance) # 150
print(account) # BankAccount(owner='Alice', balance=150)

Worth knowing: @property gives you controlled access to attributes without callers needing to change their syntax. __repr__ is what gets shown in the REPL and in error messages โ€” always implement it.


12. Importing Modules and Packages

import math
import os
from pathlib import Path
from collections import defaultdict, Counter
# Standard library โ€” always available
print(math.pi) # 3.141592653589793
print(os.getcwd()) # current working directory
# Path (prefer over os.path for most use cases)
p = Path("data") / "output.csv"
print(p.suffix) # .csv
# Counter โ€” counts hashable items
words = ["the", "cat", "sat", "on", "the", "mat", "the"]
print(Counter(words)) # Counter({'the': 3, ...})

13. Virtual Environments

Terminal window
# Create
python -m venv .venv
# Activate
source .venv/bin/activate # Linux/macOS
.venv\Scripts\activate # Windows
# Install and freeze dependencies
pip install requests pandas
pip freeze > requirements.txt
# Recreate environment from file
pip install -r requirements.txt
# Deactivate
deactivate

Worth knowing: Name your virtual environment .venv (with the dot) so itโ€™s hidden in most file explorers and automatically excluded by .gitignore templates. Never commit a virtual environment directory to source control.


14. The random Module

import random
# Random integer in range (inclusive both ends)
roll = random.randint(1, 6)
# Random float between 0 and 1
probability = random.random()
# Choose from a list
colour = random.choice(["red", "green", "blue"])
# Shuffle in place
deck = list(range(1, 53))
random.shuffle(deck)
# Reproducible results
random.seed(42)
print(random.randint(1, 100)) # Always 52 with this seed

15. Lambda Functions

# Single-expression anonymous function
square = lambda x: x ** 2
# Most useful as sort keys and in functional operations
people = [{"name": "Charlie", "age": 30}, {"name": "Alice", "age": 25}]
sorted_people = sorted(people, key=lambda p: p["age"])
print(sorted_people[0]["name"]) # Alice

Worth knowing: Lambda functions are limited to a single expression. For anything more complex, define a named function โ€” itโ€™s more readable and easier to test and debug. The main legitimate use case is short sort keys and callbacks.


16. Map, Filter, and Reduce

from functools import reduce
temperatures_c = [0, 20, 37, 100]
# map โ€” applies function to each element
to_fahrenheit = list(map(lambda c: c * 9/5 + 32, temperatures_c))
# filter โ€” keeps elements where function returns True
warm = list(filter(lambda c: c > 20, temperatures_c))
# reduce โ€” accumulates a result across all elements
total = reduce(lambda a, b: a + b, temperatures_c)
print(to_fahrenheit) # [32.0, 68.0, 98.6, 212.0]
print(warm) # [37, 100]
print(total) # 157

Worth knowing: In Python 3, list comprehensions and generator expressions are often more readable than map and filter for simple cases. Use whichever is clearer in context. reduce has no comprehension equivalent, so it stays.


17. Generators and yield

def fibonacci(limit: int):
"""Yields Fibonacci numbers up to limit without storing them all."""
a, b = 0, 1
while a <= limit:
yield a
a, b = b, a + b
# Generator expression (like list comprehension but lazy)
squares_gen = (x ** 2 for x in range(1_000_000))
# Only compute what you need
for fib in fibonacci(50):
print(fib, end=" ")
# 0 1 1 2 3 5 8 13 21 34

Worth knowing: Generators produce values on demand. A generator expression for a million items uses almost no memory; a list comprehension stores all million items. Use generators when youโ€™re processing large datasets or infinite sequences.


18. Decorators

import time
import functools
def timer(func):
"""Measure and print execution time of a function."""
@functools.wraps(func) # preserves original function metadata
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
@timer
def slow_operation():
time.sleep(0.1)
return "done"
slow_operation() # slow_operation took 0.1002s

Worth knowing: Always use @functools.wraps(func) inside your decoratorโ€™s wrapper โ€” without it, func.__name__, func.__doc__, and similar metadata are replaced by the wrapperโ€™s metadata, which breaks introspection and debugging.


19. Context Managers

from contextlib import contextmanager
@contextmanager
def managed_connection(host: str):
"""Context manager using contextlib โ€” simpler than __enter__/__exit__."""
print(f"Connecting to {host}")
connection = {"host": host, "active": True}
try:
yield connection
finally:
connection["active"] = False
print(f"Connection to {host} closed")
with managed_connection("db.example.com") as conn:
print(f"Using connection: {conn}")

Worth knowing: The @contextmanager decorator from contextlib lets you write context managers as generators without implementing __enter__ and __exit__ on a class. Itโ€™s cleaner for most use cases.


20. Unit Testing with pytest

test_calculator.py
def add(a: float, b: float) -> float:
return a + b
def divide(a: float, b: float) -> float:
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
# pytest auto-discovers functions starting with test_
def test_add_positive_numbers():
assert add(2, 3) == 5
def test_add_negative():
assert add(-1, 1) == 0
def test_divide_raises_on_zero():
import pytest
with pytest.raises(ValueError, match="Cannot divide by zero"):
divide(10, 0)
Terminal window
# Run all tests
pytest
# With verbose output
pytest -v
# Stop after first failure
pytest -x

Worth knowing: pytest is the industry standard for Python testing. Itโ€™s more expressive than the built-in unittest module and generates much more readable failure output.


21. Type Hints

from typing import Optional, Union, list as List
def find_user(user_id: int) -> Optional[dict]:
"""Return user dict or None if not found."""
users = {1: {"name": "Alice"}, 2: {"name": "Bob"}}
return users.get(user_id)
def process_value(value: Union[int, float]) -> str:
return f"Processed: {value * 2}"
# Python 3.10+ shorthand for Union
def parse_id(value: int | str) -> int:
return int(value)
result = find_user(1)
print(result) # {'name': 'Alice'}

Worth knowing: Type hints donโ€™t affect runtime behaviour โ€” Python doesnโ€™t enforce them. Their value is in tooling (mypy for static checking, IDE autocompletion) and documentation. Add them to any code thatโ€™s meant to be maintained by others.


22. Logging (the Right Way)

import logging
# Configure once at module level โ€” not inside functions
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
logger = logging.getLogger(__name__)
def process_batch(items: list):
logger.info("Starting batch of %d items", len(items))
for item in items:
try:
logger.debug("Processing: %s", item)
# ... process item
except Exception as e:
logger.error("Failed on item %s: %s", item, e, exc_info=True)
logger.info("Batch complete")
process_batch(["a", "b", "c"])

Worth knowing: Never use print() for application logging in code you expect others to run or maintain. logging gives you level control, structured output, and the ability to redirect logs without touching the code.


23. Working with HTTP APIs

import requests
from requests.exceptions import HTTPError, Timeout
def get_user(user_id: int) -> dict:
url = f"https://jsonplaceholder.typicode.com/users/{user_id}"
try:
response = requests.get(url, timeout=5)
response.raise_for_status() # raises HTTPError for 4xx/5xx
return response.json()
except HTTPError as e:
print(f"HTTP error {e.response.status_code}: {e}")
return {}
except Timeout:
print("Request timed out")
return {}
user = get_user(1)
print(user.get("name")) # Leanne Graham

Worth knowing: Always set a timeout โ€” without one, requests can hang indefinitely. Use raise_for_status() to convert HTTP error responses into exceptions rather than silently returning bad data.


24. Data Structures โ€” Stack, Queue, and Deque

from collections import deque
# Stack (LIFO) โ€” use a list
stack = []
stack.append("first")
stack.append("second")
stack.append("third")
print(stack.pop()) # "third" โ€” O(1)
# Queue (FIFO) โ€” use deque, not list
queue = deque()
queue.append("first")
queue.append("second")
print(queue.popleft()) # "first" โ€” O(1)
# list.pop(0) is O(n) because it shifts everything โ€” avoid it
# deque as a sliding window
window = deque(maxlen=3)
for n in range(6):
window.append(n)
print(list(window))

import bisect
# Manual implementation โ€” good to know for interviews
def binary_search(arr: list, target: int) -> int:
"""Return index of target, or -1 if not found."""
lo, hi = 0, len(arr) - 1
while lo <= hi:
mid = (lo + hi) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
lo = mid + 1
else:
hi = mid - 1
return -1
# Standard library implementation
sorted_list = [1, 3, 5, 7, 9, 11]
idx = bisect.bisect_left(sorted_list, 7)
print(idx) # 3
print(binary_search([1, 3, 5, 7, 9], 7)) # 3
print(binary_search([1, 3, 5, 7, 9], 6)) # -1

26. Threading and Multiprocessing

import threading
import concurrent.futures
# Threading โ€” good for I/O-bound work (API calls, file reads)
def fetch_data(url: str):
import requests
return requests.get(url).status_code
urls = [
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/1",
]
# ThreadPoolExecutor is cleaner than managing threads manually
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
results = list(executor.map(fetch_data, urls))
print(results) # [200, 200] โ€” both fetched concurrently

Worth knowing: Pythonโ€™s Global Interpreter Lock (GIL) means threads donโ€™t speed up CPU-bound work. Use multiprocessing or ProcessPoolExecutor for CPU-heavy tasks (number crunching, image processing). Use threading or asyncio for I/O-bound tasks.


27. JSON Serialisation

import json
from datetime import datetime
# Basic serialise/deserialise
data = {"name": "Alice", "scores": [95, 87, 91], "active": True}
json_str = json.dumps(data, indent=2)
restored = json.loads(json_str)
# Custom encoder for types JSON doesn't handle natively
class DateEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, datetime):
return obj.isoformat()
return super().default(obj)
payload = {"event": "login", "timestamp": datetime.now()}
print(json.dumps(payload, cls=DateEncoder))

28. SQLite Database Operations

import sqlite3
from contextlib import contextmanager
@contextmanager
def get_db(path: str = ":memory:"):
conn = sqlite3.connect(path)
conn.row_factory = sqlite3.Row # rows act like dicts
try:
yield conn
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
with get_db() as db:
db.execute("""
CREATE TABLE products (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
price REAL
)
""")
db.execute("INSERT INTO products (name, price) VALUES (?, ?)", ("Widget", 9.99))
rows = db.execute("SELECT * FROM products").fetchall()
for row in rows:
print(dict(row)) # {'id': 1, 'name': 'Widget', 'price': 9.99}

Worth knowing: Always use parameterised queries (? placeholders) rather than string formatting โ€” SQL injection is a real risk in code that handles user input, even in internal tools.


29. Flask โ€” Minimal Web Application

from flask import Flask, request, jsonify
app = Flask(__name__)
# In-memory store for demo purposes
users = {}
@app.route("/users", methods=["GET"])
def list_users():
return jsonify(list(users.values()))
@app.route("/users", methods=["POST"])
def create_user():
data = request.get_json()
if not data or "name" not in data:
return jsonify({"error": "name required"}), 400
user_id = len(users) + 1
users[user_id] = {"id": user_id, "name": data["name"]}
return jsonify(users[user_id]), 201
if __name__ == "__main__":
app.run(debug=True)

Worth knowing: debug=True enables the interactive debugger and auto-reloading, which is useful in development. Never run with debug=True in production โ€” it exposes the debugger console.


30. Code Quality and Best Practices

# Tools to run on any Python project
# pip install black isort mypy pylint
# black โ€” opinionated auto-formatter, zero config
# isort โ€” sorts and organises imports
# mypy โ€” static type checking based on type hints
# pylint โ€” linting, style, and complexity checks
# Example: well-structured function with all best practices applied
from pathlib import Path
from typing import Optional
import logging
logger = logging.getLogger(__name__)
def load_config(path: Path) -> Optional[dict]:
"""
Load JSON config from path.
Returns None if file is missing or malformed.
Logs specific errors rather than silently failing.
"""
import json
if not path.exists():
logger.warning("Config file not found: %s", path)
return None
try:
with path.open(encoding="utf-8") as f:
return json.load(f)
except json.JSONDecodeError as e:
logger.error("Invalid JSON in %s: %s", path, e)
return None

Worth knowing: The four tools above โ€” black, isort, mypy, pylint โ€” form a solid quality baseline for any Python project. Running them in CI ensures consistent style and catches type errors before they reach production. Configure them in pyproject.toml to avoid per-developer configuration drift.


Interview and Exam Quick Review

These are the patterns that come up most frequently in Python technical interviews:

TopicWhat to Know
List comprehensionsSyntax, with condition, vs map/filter
Generatorsyield, memory efficiency, next()
Decorators@functools.wraps, closure mechanics
OOP__init__, @property, __repr__, inheritance
Error handlingSpecific exceptions, else, finally
Type hintsOptional, Union, `
Context managerswith, @contextmanager
Threading vs multiprocessingGIL, I/O-bound vs CPU-bound
Binary searchManual implementation + bisect
Default mutable argsThe def f(x=[]) anti-pattern

The code patterns above are not just for reference โ€” theyโ€™re the building blocks for every larger Python programme. If you can write each one from memory and explain why itโ€™s structured the way it is, you have a solid foundation for interviews, production code, and pair programming sessions alike.