Back to cheat sheets

Languages

Python

Data types, mutability, comprehensions, generators, decorators, OOP, exceptions, the GIL, the standard library, and the pytest ecosystem — the parts that surface in SDET interviews, with worked examples.

01Data Types & Mutability

  • Immutable: int, float, bool, str, tuple, frozenset, bytes.
  • Mutable: list, dict, set, bytearray.

Everything is an object; a variable is a name bound to an object, not a typed box. Assignment binds names — it never copies. That's why aliasing surprises people:

a = [1, 2, 3]
b = a          # same object — NOT a copy
b.append(4)
print(a)       # [1, 2, 3, 4]  -- a changed too

c = a[:]       # shallow copy via slice
import copy
d = copy.deepcopy(a)   # fully independent
Classic gotcha: never use a mutable default argument (def f(x=[])) — the default is created once at definition and shared across every call. Use None as the sentinel and create inside:
def f(x=None): x = x or []

list vs tuple: list is mutable and dynamic; tuple is immutable and hashable (so it can be a dict key / set member). is compares identity (same object); == compares value.

02Comprehensions & Generators

Comprehensions build collections in one readable expression — list, dict, and set forms:

squares  = [n*n for n in range(10) if n % 2 == 0]   # list
lookup   = {u.id: u.name for u in users}            # dict
uniques  = {w.lower() for w in words}               # set
matrix   = [[r*c for c in range(3)] for r in range(3)]  # nested

Generators produce values lazily with yield, holding one item in memory at a time — ideal for large or infinite streams. A generator expression just swaps brackets for parentheses:

def read_large(path):
    with open(path) as f:
        for line in f:           # lazy: one line at a time
            yield line.strip()

total = sum(len(line) for line in read_large("huge.log"))  # gen expression

A list comprehension builds the whole list in memory; a generator yields items on demand — the memory difference is the interview point.

03Functions, *args & Decorators

  • *args collects extra positional args into a tuple; **kwargs collects extra keyword args into a dict.
  • Functions are first-class — assign them, pass them, return them, store them in lists/dicts.
  • Closures capture enclosing-scope variables; the basis of decorators.
  • Decorators wrap a function to add behavior (logging, timing, retry, caching) without changing its body. functools.wraps preserves the wrapped function's name/docstring.
import functools, time

def retry(times=3):                 # decorator WITH arguments
    def decorator(fn):
        @functools.wraps(fn)
        def wrapper(*args, **kwargs):
            for attempt in range(times):
                try:
                    return fn(*args, **kwargs)
                except Exception:
                    if attempt == times - 1:
                        raise
        return wrapper
    return decorator

@retry(times=5)
def flaky_call():
    ...
Common built-in decorators: @property, @staticmethod, @classmethod, @functools.lru_cache (memoize), and pytest's @pytest.fixture / @pytest.mark.parametrize.

04OOP in Python

  • class, __init__ constructor, self as the explicit instance reference.
  • No true private — convention: _protected, __name triggers name-mangling.
  • @classmethod (gets cls, e.g. factories), @staticmethod (no instance), @property (computed attribute).
  • Dunder methods: __str__/__repr__, __eq__, __hash__, __len__, __enter__/__exit__ (context managers), __iter__.
from dataclasses import dataclass

@dataclass(frozen=True)          # auto __init__/__repr__/__eq__; frozen = immutable + hashable
class User:
    id: int
    name: str
    role: str = "member"         # default field

    @property
    def is_admin(self) -> bool:
        return self.role == "admin"

u = User(1, "Ada", "admin")
u.is_admin          # True
User(1, "Ada", "admin") == u   # True — value equality from @dataclass

MRO & super(): Python supports multiple inheritance, resolved by the C3 linearization (method resolution order); super() follows the MRO chain.

05Exceptions & Context Managers

try / except / else / finally. else runs only if no exception was raised; finally always runs. Catch specific exceptions, not a bare except:. Raise with raise ValueError("msg"); chain causes with raise X from err; custom exceptions subclass Exception.

class ConfigError(Exception):
    pass

try:
    cfg = load(path)
except FileNotFoundError as e:
    raise ConfigError(f"missing {path}") from e   # preserve original cause
else:
    print("loaded ok")          # only if no exception
finally:
    cleanup()                    # always

Context managers (with open(...) as f:) guarantee setup/cleanup via __enter__/__exit__ — the Pythonic equivalent of try-with-resources. Write your own with @contextlib.contextmanager:

from contextlib import contextmanager

@contextmanager
def timer(label):
    start = perf_counter()
    try:
        yield                # body runs here
    finally:
        print(f"{label}: {perf_counter() - start:.3f}s")

with timer("db query"):
    run_query()

06The GIL & Concurrency

The Global Interpreter Lock lets only one thread execute Python bytecode at a time in CPython, so threads don't give true multi-core CPU parallelism.

  • threading — good for I/O-bound work (network, files, browser actions); the GIL releases during blocking I/O, so threads overlap.
  • multiprocessing — separate processes, each with its own interpreter and GIL; the way to use multiple cores for CPU-bound work.
  • asyncio — single-threaded cooperative concurrency (async/await) for very high-I/O workloads (thousands of sockets).
import asyncio, aiohttp

async def fetch(session, url):
    async with session.get(url) as r:
        return await r.text()

async def main(urls):
    async with aiohttp.ClientSession() as s:
        return await asyncio.gather(*(fetch(s, u) for u in urls))  # concurrent
For test automation: I/O-bound parallelism (API calls, browser actions) benefits from threads/async; pytest itself parallelizes across processes with pytest-xdist (-n auto), sidestepping the GIL entirely.

07Standard Library & Idioms

The batteries-included modules that come up constantly in SDET work:

ModuleUse
collectionsdefaultdict, Counter, namedtuple, deque.
itertoolsLazy combinatorics: chain, groupby, product.
jsonloads/dumps for API payloads.
pathlibObject-oriented file paths (Path("x")/"y").
os / subprocessEnv vars, running external commands.
reRegular expressions.
datetimeTimestamps, durations.
from collections import Counter, defaultdict

Counter("mississippi").most_common(2)   # [('i', 4), ('s', 4)]

groups = defaultdict(list)
for user in users:
    groups[user.dept].append(user)        # no KeyError on first insert

# f-strings — the default formatting idiom
name, n = "Ada", 3
f"{name} ran {n} test{'s' if n != 1 else ''} ({n/10:.0%})"

08pytest Ecosystem

  • Plain assert with rich introspection — pytest rewrites it to show both sides on failure; no special assert methods.
  • Fixtures (@pytest.fixture) for setup/teardown, with scopes (function/class/module/session) and dependency injection by argument name. yield separates setup from teardown.
  • @pytest.mark.parametrize for data-driven tests; pytest.raises for expected exceptions.
  • Markers (@pytest.mark.smoke), conftest.py for shared fixtures, pytest-xdist for parallel runs, monkeypatch for patching.
  • Playwright's pytest plugin gives a page fixture directly.
import pytest

@pytest.fixture(scope="module")
def api_client():
    client = ApiClient(base_url="https://test")   # setup
    yield client
    client.close()                                  # teardown

@pytest.mark.parametrize("value,expected", [(2, 4), (3, 9), (5, 25)])
def test_square(value, expected):
    assert value * value == expected

def test_raises():
    with pytest.raises(ValueError, match="negative"):
        sqrt(-1)

09Rapid-Fire Q&A

Reveal each answer to self-check, then test yourself with the quiz.

list vs tuple?

List is mutable and dynamic; tuple is immutable and hashable (can be a dict key / set member).

is vs ==?

is checks identity (same object in memory); == checks value equality via __eq__.

Mutable default argument trap?

Defaults are evaluated once at definition; a shared mutable default persists across calls — use None and create inside.

What is a generator?

A lazy iterator using yield that produces values on demand, keeping memory flat for large sequences.

What's a decorator?

A callable that wraps a function to add behavior (retry, timing, logging) without modifying its code; use functools.wraps to preserve metadata.

Explain the GIL.

Only one thread runs Python bytecode at a time in CPython; use multiprocessing for CPU-bound and threads/asyncio for I/O-bound work.

How do pytest fixtures work?

Functions marked @pytest.fixture provide setup/teardown (yield splits the two) and are injected into tests by matching argument name; scopes control reuse.

Shallow vs deep copy?

Shallow (a[:], copy.copy) copies the container but shares nested objects; copy.deepcopy duplicates everything recursively.

What does @dataclass give you?

Auto-generated __init__, __repr__, and __eq__ from typed fields; frozen=True makes instances immutable and hashable.

How do you test an expected exception?

with pytest.raises(ValueError): — the block passes only if that exception is raised.

@staticmethod vs @classmethod?

staticmethod gets no implicit first arg; classmethod gets cls and is used for alternative constructors/factories.

threading vs multiprocessing vs asyncio?

threading/asyncio for I/O-bound concurrency; multiprocessing for CPU-bound parallelism across cores.