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 independentdef 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)] # nestedGenerators 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
*argscollects extra positional args into a tuple;**kwargscollects 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.wrapspreserves 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():
...@property, @staticmethod, @classmethod, @functools.lru_cache (memoize), and pytest's @pytest.fixture / @pytest.mark.parametrize.04OOP in Python
class,__init__constructor,selfas the explicit instance reference.- No true private — convention:
_protected,__nametriggers name-mangling. @classmethod(getscls, 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 @dataclassMRO & 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() # alwaysContext 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)) # concurrentpytest-xdist (-n auto), sidestepping the GIL entirely.07Standard Library & Idioms
The batteries-included modules that come up constantly in SDET work:
| Module | Use |
|---|---|
| collections | defaultdict, Counter, namedtuple, deque. |
| itertools | Lazy combinatorics: chain, groupby, product. |
| json | loads/dumps for API payloads. |
| pathlib | Object-oriented file paths (Path("x")/"y"). |
| os / subprocess | Env vars, running external commands. |
| re | Regular expressions. |
| datetime | Timestamps, 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
assertwith 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.yieldseparates setup from teardown. @pytest.mark.parametrizefor data-driven tests;pytest.raisesfor expected exceptions.- Markers (
@pytest.mark.smoke),conftest.pyfor shared fixtures,pytest-xdistfor parallel runs,monkeypatchfor patching. - Playwright's pytest plugin gives a
pagefixture 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.