← BackPython30 Q

Python

30 questions | entry to principal | answers written to be said out loud, not read

entry

What's the difference between mutable and immutable types in Python?

Immutable objects like int, str, tuple, and frozenset cannot be changed after creation, so any operation that looks like a mutation actually creates a new object. Mutable objects like list, dict, and set can be changed in place, which means multiple references can see the same change. I care about this mostly around function arguments and default values, because passing a mutable object hands out a shared reference, not a copy.

When do you reach for a list vs a tuple vs a set vs a dict?

I use a list when I need an ordered, mutable sequence. A tuple is for a fixed, ordered group of values, often heterogeneous, and it's hashable if its contents are, so it can be a dict key. A set is for unique, unordered membership checks and set algebra like union and intersection. A dict is for key to value lookups where I need fast access by a meaningful key instead of a position.

What is a generator and how does yield work?

A generator is a function that uses yield instead of return, and calling it gives back an iterator without running any code yet. Each call to next pauses and resumes the function at the yield statement, keeping its local state alive between calls. That lets me produce a sequence of values lazily without building the whole thing in memory up front.

What's the difference between is and ==?

is checks identity, meaning both names point to the exact same object in memory. == checks equality, meaning the values compare equal, which can be true even for two different objects. I use is for None, True, and False checks because those are singletons, and == for comparing actual values like strings or numbers.

Explain *args and **kwargs.

*args collects any extra positional arguments into a tuple, and **kwargs collects extra keyword arguments into a dict. I use them when writing a function that needs to accept a flexible or unknown number of arguments, or when I'm wrapping another function and just need to forward whatever gets passed in.

What does a class give you in Python that a plain dict doesn't?

A class bundles data and behavior together, gives you a defined shape through __init__, and lets you use inheritance, methods, properties, and dunder methods to control how instances behave. A dict is fine for loose, ad hoc data, but a class documents intent, and tools like mypy or an IDE can check attribute names and types against it in a way they can't for a bag of dict keys.

Why do you prefer f-strings over .format() or % formatting?

f-strings let me embed expressions directly inside the string literal, so it's more readable and I can call functions or do arithmetic right where the value is used. They're also evaluated at runtime as bytecode rather than parsed as a separate format string, which makes them faster than .format() or % formatting in practice.

Why use pathlib instead of os.path for file paths?

pathlib gives me a Path object with an object oriented API instead of passing strings around to a bunch of separate os.path functions. Joining paths is just the slash operator, and common checks like exists, is_file, or reading text are methods right on the object. It also handles cross platform separators for me, so I don't have to think about forward versus backward slashes.

junior

How does a Python dict actually work under the hood, and is it ordered?

A dict is a hash table, so key lookup, insertion, and deletion are all average case O(1) because the key's hash points almost directly at a slot. Since Python 3.7, dicts also preserve insertion order as a language guarantee, not just an implementation detail, so iterating a dict gives you keys in the order they were added.

List comprehension vs generator expression, when do you pick one over the other?

A list comprehension builds the entire list in memory immediately, which is fine for small or reused collections. A generator expression produces values lazily, one at a time, so it's better when I'm processing a large or unbounded sequence and only need to iterate once. I reach for the generator form whenever memory matters or I'm just feeding a loop or another function like sum or any.

What's the iterator protocol, and how is an iterable different from an iterator?

An iterable is anything with an __iter__ method that returns an iterator. An iterator implements __iter__, returning itself, and __next__, which returns the next value or raises StopIteration when it's exhausted. A for loop is really just calling iter on the iterable once and then next repeatedly until StopIteration is raised.

Why is using a mutable default argument a classic Python bug?

A default argument is evaluated once, when the function is defined, not each time it's called. If that default is a mutable object like a list, every call that doesn't pass its own argument shares and mutates that same list, so state leaks across calls in a way that's really surprising. I avoid it by defaulting to None and creating a new list inside the function body.

What is a closure in Python, and what is the late binding gotcha?

A closure is a function that remembers variables from the scope it was defined in, even after that outer function has returned. The late binding gotcha shows up in loops, because a closure looks up the variable's value at call time, not at creation time, so if you create several closures inside a loop they all end up referencing the loop variable's final value. I fix it by capturing the value as a default argument.

What is a decorator and how does it work?

A decorator is a function that takes another function, wraps it with extra behavior, and returns the wrapped version. Applying @my_decorator above a function is just shorthand for reassigning that function to the decorator's return value. I use decorators for cross cutting concerns like logging, timing, caching, or access checks, and I use functools.wraps so the wrapped function keeps its original name and docstring.

What is a context manager and why use with?

A context manager guarantees setup and teardown around a block of code, using __enter__ and __exit__, so resources like files, locks, or connections get cleaned up even if an exception happens inside the block. The with statement is how I invoke one, and I reach for contextlib.contextmanager when I want to write one quickly as a generator function instead of a full class.

What problem do dataclasses solve?

A dataclass generates the boilerplate you'd otherwise write by hand for a class that mostly just holds data, like __init__, __repr__, and __eq__. I reach for it whenever a class is primarily a typed bag of fields, and I'll set frozen=True when I want it immutable and hashable, which is handy for keys or values I don't want mutated by accident.

What is duck typing and how does Python lean into it?

Duck typing means an object's suitability is determined by whether it has the methods and behavior you need, not by its declared type. Python doesn't require explicit interfaces, so if an object walks like a file and reads like a file, I can pass it wherever a file is expected. Protocols and abstract base classes let me document that expectation without forcing inheritance.

Why do you prefer pytest over the built-in unittest module?

pytest lets me write plain assert statements instead of a whole vocabulary of assertEqual and assertTrue methods, and it gives much better failure output showing exactly what didn't match. Fixtures are more flexible than unittest's setUp, since they can be scoped, shared across files, and composed. It also has a huge plugin ecosystem for things like coverage, mocking, and parametrized tests.

senior

What are dunder methods and how do they relate to Python's data model?

Dunder methods, like __init__, __repr__, __eq__, __len__, and __add__, are the hooks Python's data model uses to make built-in syntax and functions work on your own objects. Implementing __len__ lets len() work, implementing __eq__ controls what == means, and implementing __iter__ makes an object usable in a for loop. It's how Python stays consistent, custom objects can behave like built-in ones without special casing.

What does __slots__ do and when would you use it?

__slots__ tells Python to skip creating a per instance __dict__ and instead reserve fixed storage for a defined set of attributes. That saves a real amount of memory and speeds up attribute access when you're creating a huge number of small instances, but it means you can't add arbitrary new attributes later. I use it on data heavy classes where memory actually matters, not as a default habit.

How does method resolution order work with multiple inheritance?

Python uses the C3 linearization algorithm to build a consistent method resolution order, which you can inspect with ClassName.__mro__. It walks the class hierarchy left to right, depth first, but keeps a parent after all its children, so a subclass's method always wins, and super() follows that same order rather than just going straight to a single parent. I check the MRO explicitly whenever I'm debugging surprising behavior from multiple inheritance or mixins.

What's the difference between EAFP and LBYL, and which does Python favor?

LBYL means look before you leap, checking conditions before acting, like verifying a key exists before accessing it. EAFP means easier to ask forgiveness than permission, just trying the operation and catching the exception if it fails. Python culturally favors EAFP because exception handling is cheap when nothing goes wrong, and it avoids race conditions where a check can go stale between the check and the actual use.

When do you reach for threading vs multiprocessing vs asyncio?

I use threading for I/O bound work, like waiting on network calls or disk, where the GIL doesn't matter because threads are mostly blocked, not computing. I use multiprocessing for CPU bound work, since separate processes each get their own interpreter and GIL, giving real parallelism at the cost of more memory and inter process communication overhead. I use asyncio when I have a huge number of concurrent I/O bound tasks and want a single thread cooperatively switching between them without the overhead of real threads.

How does async/await actually work in Python?

async def defines a coroutine, a function that can pause at await points instead of blocking. The event loop is the thing that actually runs, it holds a queue of coroutines and switches between them whenever one hits an await on something not ready yet, like a network response. Nothing runs in true parallel here, it's cooperative concurrency on a single thread, which is why one coroutine that blocks without awaiting can freeze the whole loop.

How does Python manage memory, and what does the garbage collector actually do?

Every object has a reference count, and CPython frees it immediately once that count drops to 0. That handles most objects cleanly, but reference counting alone can't collect reference cycles, like two objects pointing at each other, so Python also runs a separate cyclic garbage collector that periodically looks for and cleans up unreachable cycles. I mostly don't think about this day to day, but it matters when I'm debugging memory that isn't being freed.

When do you reach for numpy instead of plain Python lists?

I reach for numpy when I'm doing numeric work over large arrays, because it stores data in contiguous typed memory and runs vectorized operations in compiled C code instead of a Python level loop. That gives a huge speedup for things like element wise math, linear algebra, or aggregations over big datasets. For small collections or mixed types, a plain list is simpler and the numpy overhead isn't worth it.

staff

Are type hints enforced at runtime, and what do you actually get from mypy?

Type hints are not enforced at runtime, Python happily ignores them when the code runs. What I get from mypy is static analysis, it reads the hints and catches mismatches, wrong argument types, or missing None checks before the code ever runs. I treat hints as documentation plus a safety net that a static checker can verify in CI, not as runtime validation.

How do you manage dependencies and virtual environments in a Python project?

I isolate every project in its own virtual environment so dependencies don't leak across projects or fight with the system Python. I pin dependencies in a requirements file or a pyproject.toml, and use a lock file where the tool supports it so installs are reproducible. For packaging a distributable project, pyproject.toml is the modern standard for declaring metadata, dependencies, and the build backend.

How do you approach performance work in Python?

I profile before I touch anything, using something like cProfile or a line profiler to find out where time is actually going, because intuition about hot spots is wrong more often than not. Once I know the real bottleneck, I look at algorithmic complexity first, then data structure choice, then whether the work even belongs in Python versus a C extension or a library like numpy. I avoid micro optimizing code that isn't actually on the hot path.

principal

What is the GIL and why does it exist?

The GIL, the global interpreter lock, is a single lock that only lets one thread execute Python bytecode at a time inside a CPython process. It exists because CPython's memory management, especially reference counting, isn't thread safe without it, and adding the lock kept the reference counting implementation simple. The real consequence is that threads don't give you true parallel CPU work in Python, they mostly help with I/O bound waiting.

Fast recall

GIL = one thread at a time | list comp = eager, built now | generator = lazy, one pass | is = same object | == = equal value | *args = extra positional args | **kwargs = extra keyword args | closure = remembers enclosing scope | decorator = wraps a function | dataclass = generated boilerplate class | __slots__ = fixed attrs, no dict | EAFP = try then catch | asyncio = cooperative single thread | dict = insertion ordered hash table

BH·Python·github.com/bunlongheng/study