Python
Python’s philosophy is “there should be one obvious way to do it.” Understanding idiomatic Python — not just its syntax — makes code that other Python developers will recognize and trust.
🟢 Junior
Built-in Data Structures
# list — ordered, mutable, allows duplicates
numbers = [1, 2, 3, 4, 5]
numbers.append(6)
numbers.extend([7, 8])
numbers.pop() # removes last, returns it
numbers[1:3] # [2, 3] — slicing
# dict — key-value pairs
user = {'name': 'Alice', 'age': 30}
user.get('email', 'N/A') # safe access with default
user['role'] = 'admin' # add or update
# set — unordered, unique values
tags = {'python', 'backend', 'python'} # {'python', 'backend'}
tags.add('async')
'python' in tags # True, O(1)
# tuple — immutable list
point = (1.5, 2.3)
x, y = point # unpacking
List Comprehensions
List comprehensions are the idiomatic way to build lists from iterables. They replace most for loops that build a list.
squares = [x**2 for x in range(10)]
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
even_squares = [x**2 for x in range(10) if x % 2 == 0]
# [0, 4, 16, 36, 64]
flat = [x for row in matrix for x in row]
# flattens a 2D list
Dict and set comprehensions work the same way:
word_lengths = {word: len(word) for word in ['hello', 'world']}
unique_lengths = {len(word) for word in words}
Functions and Arguments
def greet(name, *, greeting='Hello'): # '*' forces greeting to be keyword-only
return f'{greeting}, {name}!'
greet('Alice') # 'Hello, Alice!'
greet('Bob', greeting='Hi') # 'Hi, Bob!'
def log(*args, **kwargs):
# args is a tuple of positional arguments
# kwargs is a dict of keyword arguments
print(args, kwargs)
log(1, 2, name='Alice', level='info')
# (1, 2) {'name': 'Alice', 'level': 'info'}
String Formatting
name, score = 'Alice', 98.5
# f-strings (preferred, Python 3.6+)
f'Player: {name}, Score: {score:.1f}' # 'Player: Alice, Score: 98.5'
f'{score = }' # 'score = 98.5' — debug output (Python 3.8+)
# Format spec mini-language
f'{"left":<20}' # left-align, 20 chars
f'{42:06d}' # zero-padded: '000042'
f'{0.12345:.2%}' # '12.35%'
Error Handling
def read_config(path):
try:
with open(path) as f:
return json.load(f)
except FileNotFoundError:
raise RuntimeError(f'Config not found: {path}') from None
except json.JSONDecodeError as e:
raise ValueError(f'Invalid JSON in {path}: {e}') from e
finally:
pass # runs regardless of success or failure
from None suppresses the original exception in the traceback. from e chains it, showing both.
🟡 Medior
Generators and yield
A generator is a function that yields values one at a time, consuming memory for only one item at a time regardless of the total sequence size.
def read_chunks(file_path, chunk_size=8192):
with open(file_path, 'rb') as f:
while chunk := f.read(chunk_size): # walrus operator
yield chunk
for chunk in read_chunks('large_file.bin'):
process(chunk)
Generator expressions are the lazy version of list comprehensions:
total = sum(x**2 for x in range(10_000_000)) # no list in memory
itertools builds on generators: chain, islice, groupby, product, combinations — know these before writing loops.
Decorators
A decorator is a function that wraps another function, adding behavior before and/or after without modifying the original.
import functools
import time
def timer(func):
@functools.wraps(func)
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 expensive():
time.sleep(0.5)
return 42
expensive() # prints: expensive took 0.5001s
@functools.wraps(func) preserves the wrapped function’s __name__, __doc__, and other attributes — always include it.
Decorators with arguments need an extra level of nesting:
def retry(times=3, exceptions=(Exception,)):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
if attempt == times:
raise
print(f'Attempt {attempt} failed: {e}')
return wrapper
return decorator
@retry(times=3, exceptions=(ConnectionError,))
def fetch(url):
...
Context Managers
Context managers manage resources — they guarantee cleanup even if an exception occurs. The with statement calls __enter__ and __exit__.
from contextlib import contextmanager
@contextmanager
def db_transaction(conn):
try:
yield conn.cursor()
conn.commit()
except Exception:
conn.rollback()
raise
with db_transaction(conn) as cursor:
cursor.execute('INSERT INTO users VALUES (?)', (1,))
Async IO
asyncio is Python’s built-in event loop for cooperative concurrency. It is ideal for I/O-bound tasks (network, file) but provides no parallelism for CPU-bound work (the GIL still applies).
import asyncio
import aiohttp
async def fetch(session, url):
async with session.get(url) as response:
return await response.json()
async def fetch_all(urls):
async with aiohttp.ClientSession() as session:
tasks = [fetch(session, url) for url in urls]
return await asyncio.gather(*tasks)
results = asyncio.run(fetch_all(['https://api.example.com/1', 'https://api.example.com/2']))
asyncio.gather runs all coroutines concurrently on the same event loop thread. For CPU-bound work, use ProcessPoolExecutor with loop.run_in_executor.
Dataclasses and attrs
Dataclasses reduce boilerplate for data-holding classes:
from dataclasses import dataclass, field
@dataclass(frozen=True) # frozen = immutable (like a namedtuple with type hints)
class Point:
x: float
y: float
z: float = 0.0
def distance(self) -> float:
return (self.x**2 + self.y**2 + self.z**2) ** 0.5
p = Point(1.0, 2.0)
p.distance() # 2.236...
frozen=True makes the instance hashable and prevents mutation. field(default_factory=list) is required for mutable defaults like lists.
🔴 Senior
The GIL and True Parallelism
The Global Interpreter Lock (GIL) prevents multiple native threads from executing Python bytecode simultaneously. This means threading gives concurrency (useful for I/O) but not true parallelism for CPU-bound work.
For CPU-bound parallelism, use multiprocessing — each process has its own GIL:
from concurrent.futures import ProcessPoolExecutor
import os
def cpu_intensive(n):
return sum(i**2 for i in range(n))
with ProcessPoolExecutor(max_workers=os.cpu_count()) as pool:
results = list(pool.map(cpu_intensive, [10**6] * 8))
Python 3.13 introduces an experimental “free-threaded” build (--disable-gil) that removes the GIL. It is not production-ready as of 2025.
Metaclasses
A metaclass controls how a class is created — it is to a class what a class is to an instance.
class SingletonMeta(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class Database(metaclass=SingletonMeta):
def __init__(self, url):
self.url = url
db1 = Database('postgres://...')
db2 = Database('mysql://...')
db1 is db2 # True — same instance
Metaclasses are the mechanism behind ORMs (SQLAlchemy’s Base), Django’s Model, and dataclass-like frameworks.
__slots__
By default, instances store their attributes in a __dict__. __slots__ replaces this with a fixed-size array, reducing memory by 40-60% and slightly speeding up attribute access.
class Point:
__slots__ = ('x', 'y', 'z')
def __init__(self, x, y, z):
self.x, self.y, self.z = x, y, z
# Can't add arbitrary attributes:
# p = Point(1, 2, 3); p.w = 4 → AttributeError
Use __slots__ for classes where you will create millions of instances (geometry vertices, time-series data points, event records).
Type Hints and Protocol
Protocol defines structural subtyping (duck typing with type checking). Any class that implements the required methods is compatible — no explicit inheritance needed.
from typing import Protocol, runtime_checkable
@runtime_checkable
class Drawable(Protocol):
def draw(self, canvas: 'Canvas') -> None: ...
class Circle:
def draw(self, canvas):
canvas.circle(self.cx, self.cy, self.r)
class Square:
def draw(self, canvas):
canvas.rect(self.x, self.y, self.w, self.h)
def render_all(shapes: list[Drawable], canvas) -> None:
for shape in shapes:
shape.draw(canvas)
# Both Circle and Square work — they satisfy Drawable without inheriting it
Senior Gotchas
Mutable default arguments are shared across all calls — a classic beginner-trap with hidden senior implications in codebases where someone changed a function signature.
def append(item, lst=[]): # lst is created once and reused!
lst.append(item)
return lst
append(1) # [1]
append(2) # [1, 2] — unexpected!
def append(item, lst=None): # correct pattern
if lst is None: lst = []
lst.append(item)
return lst
is compares identity, == compares equality. CPython interns small integers (-5 to 256) and short strings, so x is 1 may be True in the REPL but False for large numbers.
except Exception does not catch SystemExit, KeyboardInterrupt, or GeneratorExit — those inherit from BaseException. This is intentional — you usually don’t want to swallow Ctrl+C.
Circular imports often manifest as ImportError: cannot import name X. Resolve by restructuring modules (move shared types to a types.py or models.py), or import inside the function where needed.