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.