Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
Python Interview Questions (Fundamentals)
A comprehensive guide to Python interview questions, organized by category. This collection covers fundamental syntax to advanced concepts for the 2025 edition.1. Python Basics
1. What is Python and what are its key features?
1. What is Python and what are its key features?
- Easy to Learn/Read: Enforces indentation-based blocks, which eliminates brace debates and produces visually consistent code. Python’s “executable pseudocode” reputation means onboarding junior engineers to a Python codebase takes days, not weeks.
- Interpreted: Code is executed line-by-line by the CPython interpreter (the reference implementation). No separate compilation step, though
.pycbytecode files are cached in__pycache__directories for faster subsequent imports. - Dynamically Typed: Variable types are determined at runtime, not at declaration. This enables rapid prototyping but means type errors surface at runtime. Production codebases increasingly use
mypyorpyrightwith type hints (PEP 484) to get static analysis benefits without losing flexibility. - Object-Oriented: Everything in Python is an object — even
int,str, and functions. Supports classes, multiple inheritance, and the full OOP toolkit, but also embraces functional patterns (map,filter, first-class functions). - Extensive Standard Library: “Batteries included” —
json,os,pathlib,collections,itertools,unittest,http.server, and 200+ modules ship with CPython. This means fewer external dependencies for common tasks. - Cross-platform: Runs identically on Windows, Linux, macOS, and even embedded systems (MicroPython). This is why DevOps tooling (Ansible, SaltStack) and data science libraries standardized on Python.
- Garbage Collected: Uses reference counting as the primary mechanism plus a cyclic garbage collector for handling circular references. You almost never manage memory manually, but understanding this matters when debugging memory leaks in long-running services.
- “What is the difference between CPython, PyPy, and Cython, and when would you choose each?”
- “If Python is interpreted and dynamically typed, how do teams enforce type safety in large codebases?”
- “What are the performance implications of Python being interpreted, and how have you worked around them in production?”
2. What are Python's built-in data types?
2. What are Python's built-in data types?
| Category | Types | Internal Implementation |
|---|---|---|
| Numeric | int, float, complex | int is arbitrary precision (no overflow!). float is C double (IEEE 754, 64-bit). complex stores real+imaginary as two floats. |
| Sequence | list, tuple, range, str | list is a dynamic array (over-allocates ~12.5%). tuple is a fixed-size array. str is immutable Unicode (UTF-8/UCS-2/UCS-4 depending on content). |
| Mapping | dict | Hash table using open addressing (since Python 3.6, insertion-ordered by implementation; guaranteed since 3.7). |
| Set | set, frozenset | Hash table (like dict but keys-only). Average O(1) lookup. |
| Boolean | bool | Subclass of int. True == 1, False == 0. This means True + True == 2. |
| Binary | bytes, bytearray, memoryview | bytes is immutable, bytearray is mutable. memoryview provides zero-copy slicing of binary data. |
| None Type | NoneType | Singleton — there is exactly one None object in memory. That’s why is None works and is preferred over == None. |
int has arbitrary precision. 2 ** 10000 works perfectly and returns a massive integer — no overflow. This is unlike C/Java where int wraps at 32/64 bits. The trade-off is performance: big-int operations are significantly slower than fixed-width integer arithmetic.What interviewers are really testing: Whether you understand the behavior of these types beyond just naming them. Can you explain why dict is ordered? Why bool is a subclass of int? Why memoryview exists?Red flag answer: Only listing the types without any understanding of their properties or internal representation. Not mentioning bytes/bytearray (critical for any network or file I/O work).Follow-up:- “Why is
boola subclass ofintin Python, and what surprising behaviors does this cause?” - “When would you use
memoryviewand what problem does it solve?” - “What happens internally when a Python
intexceeds 64 bits?”
3. What is the difference between lists and tuples?
3. What is the difference between lists and tuples?
| Feature | Lists | Tuples |
|---|---|---|
| Mutability | Mutable (can change in place) | Immutable (cannot change after creation) |
| Syntax | Square brackets [] | Parentheses () — though it’s the comma that makes a tuple, not the parens |
| Performance | Slower creation, more memory overhead | ~5-12% faster creation, smaller memory footprint |
| Hashability | Not hashable (can’t be dict keys) | Hashable if all elements are hashable (can be dict keys) |
| Memory | Over-allocates for amortized O(1) appends | Exact allocation, no slack space |
| Use Case | Collections that change (shopping cart items) | Fixed records (database row, coordinate pair, function return values) |
- Why tuples are faster: CPython caches small tuples (up to length 20) in a free list, so creating them avoids
malloccalls. Tuples also have a smaller memory footprint — a list of 10 elements uses ~136 bytes vs ~120 bytes for the equivalent tuple, because lists need extra space for the over-allocation growth buffer. - Named tuples: In production, raw tuples are often replaced by
collections.namedtupleortyping.NamedTuplefor readability:Point = namedtuple('Point', ['x', 'y']). This gives you tuple performance with attribute access. - The gotcha:
([1, 2],)is a tuple containing a mutable list. The tuple is immutable (you can’t replace the list), but the list inside can still be modified. This trips up many candidates.
x = 1, is a tuple).Follow-up:- “Can you have a tuple that contains mutable objects? What implications does that have for hashing?”
- “When would you use
namedtuplevs. a dataclass vs. a regular tuple?” - “In a multithreaded application, why might you prefer tuples over lists for shared data?”
4. Explain Python's mutable and immutable types.
4. Explain Python's mutable and immutable types.
- Mutable: Objects whose internal state can be modified after creation without creating a new object. The object’s
id()stays the same.- Examples:
list,dict,set,bytearray, and custom class instances (by default). - Example:
l = [1, 2]; l.append(3)—id(l)hasn’t changed, same object in memory.
- Examples:
- Immutable: Objects that cannot be changed once created. Any “modification” actually creates a brand new object with a new
id().- Examples:
int,float,str,tuple,frozenset,bytes. - Example:
s = 'hello'; s = s.upper()— this creates a new string'HELLO'and rebindssto it. The original'hello'still exists in memory (until garbage collected).
- Examples:
- Default argument trap — the single most common Python bug in production:
-
Dict keys must be hashable (immutable): You can use
(1, 2)as a dict key but not[1, 2]. This is because hash values must be stable — if an object’s contents can change, its hash would change and the dict’s internal hash table would break. -
String interning: CPython interns small strings and integers (-5 to 256) for performance. This means
a = 'hello'; b = 'hello'; a is breturnsTrue— but only because of an optimization, NOT because strings are mutable. Never rely onisfor value comparison. - Copy implications: Mutability directly affects whether you need shallow vs. deep copies. With immutable objects, “copying” is essentially free — Python just shares the reference.
x = x + 1) with mutation. Thinking strings are mutable because you can do s += 'more'.Follow-up:- “Explain the mutable default argument bug and how it has affected you in real code.”
- “Why does CPython intern small integers, and what range does it intern?”
- “If I pass a list to a function and modify it inside, does the caller see the change? Why?”
5. Identity Operators: 'is' vs 'is not'
5. Identity Operators: 'is' vs 'is not'
id()), not just if they have equal values.None checks:is for None: None is a singleton — there’s exactly one None object in the entire Python process. Using is checks identity directly (one pointer comparison, extremely fast). Using == triggers the __eq__ method, which could be overridden by a custom class to return True even when the value isn’t None.The integer caching trap:== for value comparison.What interviewers are really testing: Whether you know the is vs == distinction AND that you’ve internalized the “always use is for None” convention. Bonus points for knowing why (singleton pattern, avoiding __eq__ override issues).Red flag answer: Using == for None checks. Not knowing about integer caching. Thinking is and == are interchangeable for immutable types.Follow-up:- “Can you write a class where
x == NonereturnsTruebutx is NonereturnsFalse? Why is this dangerous?” - “Why does CPython cache integers from -5 to 256 specifically?”
- “In what scenarios would
a is breturnTruefor strings but not always reliably?”
6. Python Naming Conventions (PEP 8)
6. Python Naming Conventions (PEP 8)
- Variables/Functions:
snake_case(e.g.,calculate_total_price,get_user_by_id). - Classes:
PascalCase(e.g.,HttpClient,UserRepository). - Constants:
UPPER_SNAKE_CASE(e.g.,MAX_RETRIES = 3,DEFAULT_TIMEOUT = 30). - Private by convention: Single underscore prefix
_internal_method— signals “don’t touch this from outside,” but Python does NOT enforce it. It’s a gentleman’s agreement. - Name mangling: Double underscore prefix
__private_attr— CPython mangles this to_ClassName__private_attrto avoid name collisions in inheritance hierarchies. This is NOT security — it’s collision avoidance. - Dunder/Magic: Double underscore on both sides
__init__,__str__— reserved for Python’s protocol methods. Never invent your own dunder names. - Throwaway variables: Single underscore
_for values you don’t need:for _ in range(10):. - Module-level “exports”:
__all__list controls whatfrom module import *exposes.
flake8orrufffor linting (ruff is 10-100x faster, written in Rust)blackorruff formatfor auto-formattingisortorrufffor import ordering- Pre-commit hooks running these tools catch violations before code review
_private and __mangled. Calling double underscore “private” without understanding name mangling. Never having used a linter.Follow-up:- “What is the actual mechanism behind double-underscore name mangling? Can you still access a
__privateattribute from outside?” - “How do you enforce PEP 8 across a team of 20 developers? What tooling do you use?”
- “When would you deliberately violate PEP 8, and how would you document that decision?”
7. Equality (==) vs Identity (is) operators
7. Equality (==) vs Identity (is) operators
==checks value equality: “Do these two objects have the same content?” It calls__eq__()under the hood, which means classes can customize what “equal” means.ischecks identity: “Are these literally the same object in memory?” It comparesid()values — one pointer comparison, zero method calls.
- Use
isfor:Nonechecks (if x is None), sentinel values, and checking against singleton objects. - Use
==for: Everything else — comparing values, numbers, strings, collections. - Never use
isfor string or number comparison in production code, even if it “works” due to interning.
__eq__ override trap:is None is safer: identity cannot be spoofed.Performance difference: is is a single pointer comparison (essentially free). == may trigger arbitrarily complex __eq__ logic — for large nested dicts, this could be expensive.What interviewers are really testing: Whether you default to is for None checks (a strong Python habit), whether you understand __eq__ customization, and whether you know about CPython’s interning optimization without relying on it.Red flag answer: Using is for string comparison (“it works for me”). Not knowing that == calls __eq__. Using == None instead of is None.Follow-up:- “What happens when you compare two objects with
==and neither has defined__eq__?” - “How would you implement
__eq__and__hash__correctly for a custom class that you want to use as dict keys?” - “Why does
float('nan') == float('nan')returnFalse?”
8. Membership Operators: 'in' vs 'not in'
8. Membership Operators: 'in' vs 'not in'
__contains__: You can define in behavior for custom classes:in operations.Red flag answer: Not knowing in checks dict keys, not values. Not being aware of the O(n) vs O(1) difference between list and set membership tests. Never having optimized a hot loop by converting to a set.Follow-up:- “You have 10 million records and need to check membership frequently. What data structure do you use and why?”
- “What is the worst-case time complexity of
infor a set, and when does it degrade?” - “How does Python’s
inoperator work on a custom object that defines neither__contains__nor__iter__?“
2. Data Structures
9. List Manipulation Methods
9. List Manipulation Methods
list, with their time complexities (what most candidates forget):| Method | Behavior | Time Complexity |
|---|---|---|
append(x) | Adds x to the end | O(1) amortized |
insert(i, x) | Inserts x at index i | O(n) — shifts all elements after i |
remove(x) | Removes first occurrence of x | O(n) — linear search + shift |
pop() | Removes and returns last element | O(1) |
pop(i) | Removes and returns element at index i | O(n) — shifts elements |
extend(iter) | Appends all elements from iterable | O(k) where k = len(iter) |
sort() | Sorts in-place (Timsort) | O(n log n) |
reverse() | Reverses in-place | O(n) |
index(x) | Returns index of first x | O(n) |
count(x) | Counts occurrences of x | O(n) |
appendvsextend:l.append([1,2])adds[1,2]as a single element;l.extend([1,2])adds1and2separately. Confusing these is a common bug.sort()vssorted():sort()mutates in-place and returnsNone.sorted()returns a new list. Writingx = my_list.sort()setsxtoNone— a very common mistake.remove()only removes the first occurrence: If you need to remove all occurrences, use a list comprehension:[x for x in items if x != target].
collections.deque which gives O(1) for both ends.insert(0) and pop(0) are O(n)), and whether you know when to reach for deque instead.Red flag answer: Not knowing sort() returns None. Not knowing insert(0, x) is O(n). Confusing append and extend.Follow-up:- “Why is
list.pop(0)O(n) butdeque.popleft()O(1)? What’s the underlying data structure difference?” - “How does Python’s list over-allocation strategy work, and why does
appendhave amortized O(1) complexity?” - “You’re building a queue. Why should you NOT use a regular list?”
10. List Comprehensions
10. List Comprehensions
for loops.Syntax: [expression for item in iterable if condition]LIST_APPEND bytecode instruction that avoids the method lookup overhead of list.append() in a regular loop. In benchmarks, comprehensions are typically 20-30% faster than equivalent for loop + append patterns.Dict and set comprehensions (often overlooked):- When the logic requires multiple statements or side effects
- When nesting gets beyond 2 levels (readability drops off a cliff)
- When you need error handling (
try/exceptinside a comprehension is not possible)
- “What is the difference between a list comprehension and a generator expression in terms of memory and performance?”
- “Can you use walrus operator (
:=) inside a comprehension? Give an example.” - “At what point do you stop using comprehensions and switch to a regular loop?”
11. Dictionaries and Key Methods
11. Dictionaries and Key Methods
- Hash table using open addressing with a compact, insertion-ordered layout
- Keys must be hashable (immutable types, or objects with stable
__hash__) - Average case: O(1) for get/set/delete. Worst case: O(n) if all keys hash-collide (extremely rare in practice)
- Dicts are insertion-ordered as of Python 3.7 (guaranteed by spec, was implementation detail in 3.6)
defaultdict — the production workhorse:Counter for frequency analysis:defaultdict/Counter instead of writing manual existence checks. Whether you know dicts are ordered in Python 3.7+. Whether you understand hashability requirements for keys.Red flag answer: Using if key in dict: dict[key].append(...) instead of defaultdict(list). Not knowing dicts are ordered in modern Python. Not knowing about the | merge operator.Follow-up:- “What happens if you modify a dictionary while iterating over it? How do you handle that safely?”
- “Explain how Python’s dict achieves O(1) average lookup. What causes worst-case O(n)?”
- “When would you use
OrderedDictfromcollectionsnow that regular dicts are ordered?”
12. Sets vs Frozensets
12. Sets vs Frozensets
- Set: Mutable, unordered collection of unique, hashable elements. Uses a hash table internally (like a dict with only keys). Supports
add(),remove(),discard(),pop(). - Frozenset: Immutable version of a set. Because it’s immutable, it’s hashable — meaning it can be used as a dict key or an element of another set.
add(): O(1) averageremove(x): O(1) average, raisesKeyErrorif missingdiscard(x): O(1) average, does NOT raise if missing (prefer this in production)x in set: O(1) average — this is the primary reason to use sets- Set operations (union, intersection): O(min(len(s1), len(s2))) for intersection
- “How would you find duplicate elements in a list of 10 million items efficiently?”
- “What happens to set performance when you have a custom class with a bad
__hash__that always returns the same value?” - “When would you use a set vs. a dict with dummy values?”
13. Set Operations
13. Set Operations
| Operation | Operator | Method | What it returns |
|---|---|---|---|
| Union | A | B | A.union(B) | All elements from both sets |
| Intersection | A & B | A.intersection(B) | Only elements in both sets |
| Difference | A - B | A.difference(B) | Elements in A but not in B |
| Symmetric Diff | A ^ B | A.symmetric_difference(B) | Elements in either but not both |
| Subset | A <= B | A.issubset(B) | True if all of A is in B |
| Superset | A >= B | A.issuperset(B) | True if A contains all of B |
| Disjoint | — | A.isdisjoint(B) | True if no overlap |
- (difference) and ^ (symmetric difference).Follow-up:- “How would you use set operations to implement a simple role-based access control system?”
- “What is the time complexity of set intersection? Does the order of operands matter?”
- “How would you find all users who are in Group A but not in Group B using set operations?”
14. Sequence Operations (Slicing/Indexing)
14. Sequence Operations (Slicing/Indexing)
sequence[start:stop:step]. All three parameters are optional.- Slicing never raises IndexError:
[1,2,3][100:]returns[], not an error. Indexing does raise:[1,2,3][100]isIndexError. - Slices create copies:
new = old[:]creates a shallow copy. This is important — modifyingnewwon’t affectold(for the top level; nested objects are still shared). - Named slices for readability:
slice()objects: Theslicebuiltin creates reusable slice objects.s = slice(1, 10, 2); lst[s]is equivalent tolst[1:10:2]. Useful for dynamic slicing.
stop is inclusive or exclusive. Not knowing negative indexing. Not knowing that slicing creates a copy.Follow-up:- “What is the difference between
lst[:]andlst.copy()andlist(lst)?” - “How would you implement
__getitem__to support slicing in a custom class?” - “Explain what happens internally when you do
lst[1:3] = [10, 20, 30]on a list.”
15. How do you sort a list of dictionaries?
15. How do you sort a list of dictionaries?
sorted() with a key function. This is tested frequently because it touches lambdas, operator module, stability, and custom comparisons.operator.itemgetter is preferred in production:- It’s implemented in C, making it ~20-40% faster than an equivalent lambda for large lists
- It’s more readable for multi-key sorts:
itemgetter('age', 'name')vslambda x: (x['age'], x['name']) - It’s picklable (lambdas are not), which matters for multiprocessing
sorted() vs .sort(), whether you reach for operator.itemgetter over lambdas, and whether you understand sort stability.Red flag answer: Only knowing the lambda approach. Not knowing about operator.itemgetter. Not understanding that list.sort() returns None. Implementing a custom bubble sort instead of using built-in Timsort.Follow-up:- “What sorting algorithm does Python use internally, and why was it chosen?”
- “How would you sort a list of objects that don’t have a natural ordering? What about using
functools.total_ordering?” - “You need to get the top 10 items from a list of 10 million. Is
sorted()the best approach?”
16. Deep Copy vs Shallow Copy
16. Deep Copy vs Shallow Copy
- Shallow Copy: Creates a new outer container, but elements inside still reference the same objects. Changes to nested objects are visible in both copies.
- Deep Copy: Creates a new outer container AND recursively copies every nested object. The result is completely independent.
- Circular references:
deepcopyhandles them via a memo dict that tracks already-copied objects - Performance: Deep copying a large nested structure is expensive. A 10MB nested dict takes ~100ms to deepcopy vs ~1ms for shallow copy
- Unpicklable objects:
deepcopymay fail on objects with file handles, database connections, or locks. You can customize behavior with__copy__and__deepcopy__methods.
dict.copy(), list[:], etc., are all SHALLOW. Whether you understand the performance implications of deep copy.Red flag answer: Thinking .copy() creates a deep copy. Not knowing what happens with nested mutable objects. Never having encountered this bug in real code.Follow-up:- “You have a deeply nested config dict that gets passed to 100 microservices. How do you prevent accidental mutation?”
- “What happens when
deepcopyencounters a circular reference?” - “How would you implement
__deepcopy__on a custom class that holds a database connection?“
3. Object-Oriented Programming
17. Four Pillars of OOP
17. Four Pillars of OOP
-
Encapsulation: Bundling data and methods that operate on that data into a single unit (class), and controlling access to internal state.
- In Python, there are no true private members (unlike Java/C++). Convention uses
_private(single underscore) and__mangled(double underscore, triggers name mangling). - Real-world example: A
BankAccountclass where_balanceis internal. External code usesdeposit()andwithdraw()methods that enforce business rules (no negative balance) instead of directly modifying_balance.
- In Python, there are no true private members (unlike Java/C++). Convention uses
-
Abstraction: Hiding complex implementation details and exposing only the necessary interface.
- Python uses
abc.ABCand@abstractmethodfor abstract base classes. - Real-world example: A
PaymentProcessorABC definesprocess_payment()as abstract.StripeProcessorandPayPalProcessorimplement the details. Calling code only knows the interface.
- Python uses
-
Inheritance: Creating new classes that inherit behavior from existing ones (code reuse and specialization).
- Python supports multiple inheritance (unlike Java). This is powerful but dangerous — the Method Resolution Order (MRO) determines which parent’s method gets called.
- Real-world preference: Composition over inheritance in most modern Python code. “Has-a” relationships (a
Carhas anEngine) are usually better than “is-a” (ElectricCaris-aCar), because inheritance hierarchies become rigid and fragile.
-
Polymorphism: Different objects responding to the same method call differently.
- Python achieves this through duck typing: “If it walks like a duck and quacks like a duck, it’s a duck.” You don’t need a shared base class — just implement the same method.
- Real-world example:
len()works on strings, lists, dicts, and any object with__len__. That’s polymorphism without inheritance.
abc.ABC. Blindly advocating deep inheritance hierarchies.Follow-up:- “Explain Python’s MRO (Method Resolution Order) and the C3 linearization algorithm.”
- “When would you choose composition over inheritance? Give a concrete example.”
- “How does duck typing relate to the EAFP (Easier to Ask Forgiveness than Permission) principle in Python?”
18. Creating a Class and __init__
18. Creating a Class and __init__
__init__ is the initializer (not the constructor — __new__ is the actual constructor that creates the object). __init__ sets up the object’s initial state after __new__ has created it.__init__vs__new__:__new__creates the instance (allocates memory),__init__initializes it. You almost never override__new__unless implementing singletons, immutable types, or metaclass patterns.- Class vs instance attributes:
speciesis shared —Person.speciesaffects all instances.nameis per-instance. If you accidentally define a mutable class attribute (like a list), all instances share it — a classic bug. selfis explicit: Unlike Java/C++ wherethisis implicit, Python forces you to declareselfas the first parameter. This is a deliberate design choice for readability.
dataclass (Python 3.7+):__init__ vs __new__, class vs instance attributes, and whether you’d reach for dataclass in modern code.Red flag answer: Calling __init__ a “constructor.” Not knowing about class attributes vs instance attributes. Not knowing dataclass exists.Follow-up:- “When would you override
__new__instead of__init__?” - “What happens if you define a mutable default like
items=[]as a class attribute?” - “Compare
dataclassvsNamedTuplevsattrs— when would you use each?”
19. Instance vs Class vs Static Methods
19. Instance vs Class vs Static Methods
| Method Type | Decorator | First Param | Can Access |
|---|---|---|---|
| Instance | (none) | self | Instance + class state |
| Class | @classmethod | cls | Class state only (no instance) |
| Static | @staticmethod | (none) | Nothing — pure utility |
@classmethod matters for inheritance:margherita had been a static method calling Pizza(...) directly, it would always return a Pizza, breaking the inheritance chain.When to use each:- Instance method: 95% of the time. When you need access to instance data.
- Class method: Alternative constructors (
from_json,from_csv,create_default), factory patterns, and methods that need to work correctly with inheritance. - Static method: Pure utility functions that logically belong to the class but don’t need any class/instance state. Controversial — some argue these should just be module-level functions.
@classmethod, and whether you know that cls enables correct inheritance behavior (vs. hardcoding the class name).Red flag answer: Saying static methods are “the same as regular functions.” Not knowing the factory pattern use of @classmethod. Not understanding how cls interacts with inheritance.Follow-up:- “Why might you choose a
@classmethodfactory over__init__with different parameter sets?” - “Should
@staticmethodeven exist, or should those always be module-level functions?” - “How do these method types interact with Python’s descriptor protocol?”
20. Inheritance and super()
20. Inheritance and super()
super() is the mechanism for calling methods from parent classes, and understanding it properly requires knowing the MRO (Method Resolution Order).super() follows the MRO, not just “the parent class.”Why super() follows MRO, not just the direct parent:
In cooperative multiple inheritance, super() calls the next class in the MRO, not necessarily the direct parent. This is critical for the diamond pattern to work correctly:super() follows MRO (not just parent), whether you can explain the diamond problem, and whether you know when to use composition instead.Red flag answer: Thinking super() always calls the direct parent. Not knowing about MRO or C3 linearization. Creating 5+ level deep inheritance hierarchies without considering composition.Follow-up:- “Explain how C3 linearization works and why Python chose it over depth-first search.”
- “What happens if you call
super()in a class that uses multiple inheritance and one parent doesn’t callsuper()?” - “You’re designing a plugin system. Would you use inheritance or composition? Why?”
21. Magic Methods (Dunder Methods)
21. Magic Methods (Dunder Methods)
len(obj), Python calls obj.__len__(). When you write a + b, Python calls a.__add__(b).Core protocols:| Category | Methods | Triggered By |
|---|---|---|
| Creation | __new__, __init__, __del__ | Object lifecycle |
| String | __str__, __repr__, __format__ | str(), repr(), f"{}" |
| Comparison | __eq__, __lt__, __le__, __gt__, __ge__ | ==, <, <=, >, >= |
| Arithmetic | __add__, __sub__, __mul__, __truediv__ | +, -, *, / |
| Container | __len__, __getitem__, __setitem__, __contains__ | len(), [], in |
| Context | __enter__, __exit__ | with statement |
| Callable | __call__ | obj() |
| Iteration | __iter__, __next__ | for loops |
| Hashing | __hash__ | hash(), dict keys, set membership |
__repr__ vs __str__ — the rule you must know:__repr__is for developers (debugging). Should be unambiguous. Ideally,eval(repr(obj))recreates the object.__str__is for end users. Human-readable.- If only one is defined, define
__repr__. Python falls back to__repr__when__str__is missing, but NOT the reverse.
__eq__/__hash__ contract:
If you define __eq__, you MUST also define __hash__ if you want the object to be usable as a dict key or set element. If you define __eq__ without __hash__, Python sets __hash__ to None, making the object unhashable. Objects that are equal MUST have the same hash.What interviewers are really testing: Whether you know the __repr__ vs __str__ distinction, the __eq__/__hash__ contract, and whether you can use dunders to make pythonic, operator-friendly classes.Red flag answer: Only knowing __init__ and __str__. Not knowing the __eq__/__hash__ contract. Defining __eq__ without __hash__ and then wondering why your objects can’t be dict keys.Follow-up:- “What happens if two objects are
__eq__but have different__hash__values?” - “How would you make a custom class work with the
withstatement?” - “Explain the difference between
__add__and__radd__. When is__radd__called?”
22. Property Decorators (@property)
22. Property Decorators (@property)
@property lets you define methods that are accessed like attributes, providing a clean API while encapsulating validation, computation, or lazy loading behind the scenes.@property matters in production:- API stability: You start with a simple attribute, then later add validation/computation without changing the caller’s code.
user.namestays the same whether it’s a raw attribute or a property. - Lazy loading: Expensive computations can be deferred until first access.
- Caching with
functools.cached_property(Python 3.8+):
@property works under the hood:
@property is a descriptor. It implements __get__, __set__, and __delete__. When you access obj.x and x is a descriptor on the class, Python calls x.__get__(obj, type(obj)) instead of returning the descriptor itself. This is the same mechanism behind @classmethod, @staticmethod, and bound methods.What interviewers are really testing: Whether you use properties to build clean APIs, whether you know about cached_property, and whether you understand the descriptor protocol that powers properties.Red flag answer: Not knowing about setters/deleters. Using Java-style get_name() / set_name() methods instead of properties. Not knowing about cached_property for expensive computations.Follow-up:- “What is the descriptor protocol and how does
@propertyuse it?” - “What’s the difference between
@propertyand@cached_property? When is each appropriate?” - “How would you implement a property that validates type, not just value (e.g., ensuring an attribute is always an integer)?“
4. Functions and Decorators
25. What are *args and **kwargs?
25. What are *args and **kwargs?
*args and **kwargs provide flexible function signatures that accept arbitrary numbers of arguments.*args: Collects extra positional arguments into a tuple.**kwargs: Collects extra keyword arguments into a dict.
- Regular positional parameters
*args- Keyword-only parameters (after
*argsor after bare*) **kwargs
* trick):*args is a tuple (not a list). Not knowing about keyword-only arguments. Never having used *args/**kwargs in decorator patterns.Follow-up:- “What is the difference between
*and**when used in function calls vs function definitions?” - “How would you use keyword-only arguments to design a safer API?”
- “Can you have positional-only parameters in Python? How?”
26. Lambda Functions
26. Lambda Functions
def would be overkill.- Single expression only — no statements, no assignments, no multi-line logic
- Cannot contain
try/except,if/elsestatements (only ternary expressions) - No docstrings, no type hints
- Not picklable (can’t be serialized for multiprocessing)
- Harder to debug: tracebacks show
<lambda>instead of a meaningful name
- Use lambda: Short sort keys, quick
filter/mapcallbacks, simple event handlers - Use
def: Anything reused, anything needing documentation, anything with complex logic - Use
operatormodule instead of lambda:operator.itemgetter('key')is faster and clearer thanlambda x: x['key']
i, not the value at the time of creation. By the time the lambda runs, the loop is done and i == 4.What interviewers are really testing: Whether you know the closure gotcha, whether you know when lambdas are appropriate vs. overkill, and whether you’d prefer operator.itemgetter for sort keys.Red flag answer: Using lambdas for complex logic. Not knowing the closure capture gotcha. Not knowing that lambdas can’t contain statements.Follow-up:- “Explain the late-binding closure bug with lambdas in loops. How do you fix it?”
- “Why would you prefer
operator.itemgetteroroperator.attrgetterover a lambda?” - “Are lambdas more or less efficient than regular functions? Why?”
27. What are Decorators?
27. What are Decorators?
functools.wraps — the detail that separates juniors from seniors:
Without @functools.wraps(func), the wrapper function replaces the original’s metadata. slow_function.__name__ would be 'wrapper' instead of 'slow_function'. This breaks debugging, logging, documentation generation, and any code that introspects function names. Always use @functools.wraps.Decorators with arguments (the double-wrapper pattern):@functools.lru_cache— memoization with LRU eviction@functools.cached_property— lazy, cached attribute computation@contextlib.contextmanager— turns a generator into a context manager@dataclasses.dataclass— auto-generates boilerplate methods@app.route(Flask) — URL routing@login_required(Django) — authentication enforcement
@functools.wraps. Whether you can write decorators with arguments. Whether you understand stacking order. Whether you’ve used decorators for real cross-cutting concerns (caching, retries, auth).Red flag answer: Forgetting @functools.wraps. Not knowing how to write decorators with arguments. Only knowing @staticmethod/@classmethod as decorators.Follow-up:- “What happens when you stack multiple decorators? What’s the execution order?”
- “How would you write a decorator that works on both sync and async functions?”
- “Explain
functools.lru_cache— how does it work, what are the gotchas, and when would you NOT use it?”
28. Closures
28. Closures
__closure__ attribute is a tuple of cell objects, each containing one captured variable. The __code__.co_freevars tuple lists the names of the captured variables.The nonlocal keyword — modifying closure variables:nonlocal, assigning to count inside increment creates a new local variable, and the += 1 operation would raise UnboundLocalError because it reads before assigning.Real-world use cases:- Factory functions (like
make_multiplierabove) - Decorators (the wrapper function is a closure over the decorated function)
- Callback registration with pre-bound parameters
- Lightweight alternative to classes when you need just state + one or two functions
nonlocal, and whether you can explain the relationship between closures and decorators.Red flag answer: Not knowing the late binding gotcha (closures capture variables, not values). Not knowing nonlocal. Confusing closures with regular nested functions that don’t capture anything.Follow-up:- “What is the difference between
globalandnonlocal?” - “Explain the late binding behavior of closures and how it causes bugs in loops.”
- “When would you use a closure instead of a class, and vice versa?”
31. Generators and yield
31. Generators and yield
yield. They don’t compute or store the entire sequence in memory — they produce each value on demand.yield from — delegating to sub-generators (Python 3.3+):send() — two-way communication with generators (coroutines):__iter__ and __next__.What interviewers are really testing: Whether you understand the memory implications, whether you’ve used generators for real data processing, and whether you know about yield from and send().Red flag answer: Not knowing the memory difference between [x for x in ...] and (x for x in ...). Not knowing generators are single-use (can’t iterate twice). Not knowing yield from.Follow-up:- “What happens if you try to iterate over a generator a second time?”
- “How would you implement a generator-based pipeline for processing streaming data?”
- “Explain how
yield fromdiffers from aforloop withyieldinside it — what extra functionality does it provide?“
5. File Handling and I/O
33. How do you read and write files?
33. How do you read and write files?
with statement (context manager) to ensure files are closed properly, even if exceptions occur. This is non-negotiable in production code.-
Always specify
encoding='utf-8': Without it, Python uses the system default encoding (oftencp1252on Windows,utf-8on Linux/Mac). This causes cross-platform bugs. PEP 686 in Python 3.15 will make UTF-8 the default. -
File modes explained:
'r'— read (default). File must exist.'w'— write. Truncates (erases) existing content!'a'— append. Adds to end.'x'— exclusive create. Fails if file exists (prevents accidental overwrites).'b'— binary mode ('rb','wb'). For images, PDFs, any non-text data.'+'— read+write ('r+','w+').
-
pathlibis the modern way:
- Buffering and flushing:
with, whether you specify encoding, whether you use pathlib in modern code, and whether you know the difference between file modes (especially 'w' truncating vs 'a' appending).Red flag answer: Not using with statement. Not specifying encoding. Using os.path.join instead of pathlib. Using f.read() on a multi-GB file.Follow-up:- “What happens if an exception occurs inside a
withblock? Is the file still closed?” - “How would you safely write to a file without risking data corruption if the process crashes mid-write?”
- “How does Python’s file buffering work, and when would you need to change the buffer size?”
36. Working with JSON
36. Working with JSON
json module handles serialization/deserialization, but there are important nuances for production use.| Python | JSON | Gotcha |
|---|---|---|
dict | object | JSON keys are always strings. {1: 'a'} becomes {"1": "a"} |
list, tuple | array | Tuples become arrays — round-trip loses tuple type |
True/False | true/false | Case difference matters |
None | null | |
int/float | number | JSON has no int/float distinction |
set | — | Not serializable by default! |
datetime | — | Not serializable by default! |
json vs orjson vs ujson:
For high-throughput APIs (processing thousands of requests/sec), the stdlib json module is a bottleneck. orjson (Rust-based) is 3-10x faster and handles datetime, numpy arrays, and dataclass natively. In a FastAPI app, switching from json to orjson can cut serialization time from 15ms to 2ms per request.What interviewers are really testing: Whether you know the type mapping gotchas (sets, datetime, int keys), whether you can write custom encoders, and whether you know about performance alternatives for high-throughput scenarios.Red flag answer: Not knowing the dumps/dump vs loads/load distinction. Not knowing that sets and datetimes aren’t serializable. Never having heard of orjson.Follow-up:- “How would you handle serializing a Python object with circular references to JSON?”
- “What is the security risk of
json.loadson untrusted input? Compare topickle.loads.” - “How would you validate the structure of incoming JSON in a REST API? (JSON Schema, Pydantic, etc.)“
6. Exception Handling
39. try-except Blocks
39. try-except Blocks
- NEVER use bare
except:— it catches EVERYTHING, includingKeyboardInterruptandSystemExit, making your program unkillable:
-
Don’t silently swallow exceptions —
except: passis called “error swallowing” and it hides bugs for weeks. At minimum, log the exception. -
Use
raiseto re-raise — if you can’t fully handle the error, log it and re-raise:
- Exception chaining (Python 3):
except: (instant red flag), whether you silently swallow exceptions, and whether you understand EAFP vs LBYL.Red flag answer: Using bare except:. Using except: pass without logging. Not knowing about exception chaining with from e. Wrapping entire functions in try-except instead of targeting specific operations.Follow-up:- “What is the difference between
raiseandraise einside an except block?” - “Explain EAFP vs LBYL with a concrete example. Which does Python prefer?”
- “How would you create a custom exception hierarchy for a library?”
41. try-except-else-finally
41. try-except-else-finally
try statement has four blocks, and knowing when each runs is critical:else matters (and why most devs get it wrong):
The else block runs only when the try block succeeds without exceptions. The key benefit: code in else is NOT protected by the except — so if use_result() raises, it’s NOT caught here. This keeps error handling precise.finally guarantees (and the return value gotcha):
finally runs even if:- An exception was raised and not caught
- A
returnstatement was executed intryorexcept - A
breakorcontinuewas executed
return in a finally block — it silently swallows exceptions and overrides previous returns.Real-world pattern — database transactions:else block exists and why it’s important (most juniors don’t), and whether you understand the finally + return gotcha.Red flag answer: Not knowing else exists. Putting all post-success logic inside the try block. Putting return in a finally block. Not understanding that finally always runs.Follow-up:- “What happens if both the
exceptblock and thefinallyblock raise exceptions?” - “How does
finallyinteract with generators andyield?” - “When would you use
finallyvs. a context manager (withstatement)?“
8. Advanced Concepts
51. Iterators vs Iterables
51. Iterators vs Iterables
for loop machinery and understanding it unlocks generators, custom iteration, and lazy evaluation.- Iterable: Any object that can return an iterator. It implements
__iter__()which returns a fresh iterator. Examples:list,tuple,str,dict,set,range,file objects. - Iterator: An object that tracks position during iteration. It implements both
__iter__()(returns self) and__next__()(returns next value or raisesStopIteration).
for loop protocol:- Iterables can be iterated multiple times (each call to
__iter__returns a fresh iterator) - Iterators are single-use — once exhausted, calling
next()keeps raisingStopIteration
iter() and next().Red flag answer: Confusing iterables and iterators. Not knowing iterators are single-use. Not understanding the StopIteration protocol.Follow-up:- “How does
itertools.chainwork internally? Is it lazy?” - “What is the difference between
__iter__returningselfvs returning a new iterator object?” - “Name three
itertoolsfunctions you use regularly and explain their use cases.”
52. Multithreading vs Multiprocessing
52. Multithreading vs Multiprocessing
| Aspect | Multithreading | Multiprocessing |
|---|---|---|
| Memory | Shared memory space | Separate memory per process |
| GIL Impact | Blocked for CPU-bound (only one thread runs Python bytecode at a time) | Bypasses GIL entirely (each process has its own interpreter) |
| Best for | I/O-bound tasks (network calls, disk reads, DB queries) | CPU-bound tasks (math, image processing, ML training) |
| Overhead | Low (threads are lightweight) | High (process creation, IPC serialization) |
| Communication | Shared objects, queue.Queue, locks | multiprocessing.Queue, Pipe, shared memory |
| Debugging | Race conditions, deadlocks | Serialization errors, zombie processes |
- I/O-bound (waiting on network/disk): Use
threadingor, better yet,asyncio - CPU-bound (number crunching): Use
multiprocessingorconcurrent.futures.ProcessPoolExecutor - Mixed: Use
asynciowithloop.run_in_executor()for the CPU-bound parts
concurrent.futures over raw threading/multiprocessing:- Uniform API for both thread and process pools
- Built-in
Futureobjects for tracking completion - Easier exception handling
as_completed()for processing results as they finish
asyncio is often better than threads because it avoids the overhead of OS thread context switching:concurrent.futures in production.Red flag answer: Using threads for CPU-bound work. Not knowing what the GIL is. Using raw threading.Thread instead of concurrent.futures. Not knowing asyncio exists.Follow-up:- “You have a web scraper that needs to fetch 10,000 URLs. How do you design it?”
- “What are the common pitfalls of shared state in multithreaded Python?”
- “How does
asynciodiffer from threading, and when would you choose one over the other?”
53. Global Interpreter Lock (GIL)
53. Global Interpreter Lock (GIL)
- Prevents multiple threads from executing Python bytecode simultaneously
- Protects CPython’s internal data structures (reference counts, object allocations) from race conditions
- Is released during I/O operations (file reads, network calls,
time.sleep), C extensions, and certain NumPy operations
- Pro: Makes single-threaded code faster (no locking overhead on every object operation). Makes C extension writing simpler.
- Con: CPU-bound multithreaded Python code gets zero speedup from multiple cores. A 4-thread CPU-bound program runs at ~1x speed, not ~4x.
counter += 1 compiles to multiple bytecodes (LOAD_GLOBAL, LOAD_CONST, BINARY_ADD, STORE_GLOBAL), and the GIL can switch threads between any of them.Workarounds for CPU-bound parallelism:multiprocessing— separate processes, each with its own GIL- C extensions (NumPy, Pandas) — release the GIL during computation
Cythonwithnogil— write C-speed Python that releases the GILconcurrent.futures.ProcessPoolExecutor— simplest process-based parallelism
--disable-gil). This removes the GIL entirely, enabling true multithreaded parallelism for CPU-bound tasks. As of 2025, it’s still experimental and not all C extensions support it.What interviewers are really testing: Whether you understand why the GIL exists (not just that it exists), that it doesn’t prevent application-level race conditions, and what the concrete workarounds are.Red flag answer: Thinking the GIL prevents all race conditions. Not knowing that the GIL is released during I/O. Saying “Python can’t do parallel programming” (it can, via multiprocessing). Not knowing about the PEP 703 no-GIL work.Follow-up:- “Does the GIL make Python thread-safe? If not, what race conditions can still occur?”
- “How do NumPy and Pandas achieve parallel performance despite the GIL?”
- “What is PEP 703 and what’s the status of removing the GIL from CPython?“
10. Data Science & Numerical Python
65. What is NumPy?
65. What is NumPy?
- Contiguous memory:
ndarraystores elements in a contiguous C-array, unlike Python lists which store pointers to scattered objects. This means CPU cache lines are used efficiently. - Vectorized operations: Operations happen in compiled C/Fortran, not interpreted Python.
np.array * 2is a single C call, not a Python loop. - No type checking per element: All elements share one dtype, so no per-element type dispatch.
- BLAS/LAPACK integration: Linear algebra ops use highly optimized libraries that leverage CPU SIMD instructions.
- Broadcasting: Arrays of different shapes can operate together:
np.array([1,2,3]) + 10adds 10 to each element. - Views vs copies: Slicing a NumPy array returns a view (no copy!). Modifying the view modifies the original. Use
.copy()for independent arrays. - dtype: Specify data type for memory control:
np.array([1,2,3], dtype=np.float32)uses 4 bytes per element instead of 8.
- “What is broadcasting and when does it fail?”
- “How do you avoid accidental data corruption from NumPy views?”
- “When would you use
float32vsfloat64?”
66. What is Pandas?
66. What is Pandas?
- Series: 1D labeled array (like a column in a spreadsheet)
- DataFrame: 2D labeled table (like a spreadsheet or SQL table)
- Never iterate rows with
iterrows()— it’s 100-1000x slower than vectorized ops. Use.apply(), vectorized NumPy operations, or.valuesfor bulk processing. - Use appropriate dtypes:
categorydtype for low-cardinality strings saves 90%+ memory.int8for small integers. Downcasting a 10GB DataFrame to proper dtypes can reduce it to 2GB. query()for readable filtering:df.query('age > 28 and salary > 60000')beats chained boolean indexing for readability.- Chained indexing warning:
df['col'][0] = valmay not modify the DataFrame (creates a copy). Usedf.loc[0, 'col'] = valinstead.
- Over ~10GB: Use
Polars(Rust-based, 5-10x faster) orDask(parallel Pandas) - For SQL-heavy workflows: DuckDB can query DataFrames directly with SQL
- For streaming data: Pandas is batch-only; use Spark or Flink
for index, row in df.iterrows() for everything. Not knowing about loc vs iloc. Not knowing when to move beyond Pandas to Polars/Dask.Follow-up:- “How would you handle a 50GB CSV file that doesn’t fit in memory?”
- “Explain the difference between
loc,iloc, andatin Pandas.” - “What is Polars and when would you choose it over Pandas?”
Additional Important Topics
76. enumerate() vs zip()
76. enumerate() vs zip()
enumerate(iterable, start=0): Wraps an iterable and returns (index, element) tuples. Replaces the anti-pattern of for i in range(len(items)).zip(*iterables): Pairs up elements from multiple iterables. Stops at the shortest iterable (use itertools.zip_longest to pad).zip gotcha with unequal lengths:zip and enumerate return iterators, not lists. They produce values on demand, so zip(range(1_000_000), range(1_000_000)) uses constant memory.What interviewers are really testing: Whether you write Pythonic loops (enumerate over range(len(…))), whether you know about strict=True in zip, and whether you can use zip for dict construction and transposing.Red flag answer: Using range(len(items)) instead of enumerate. Not knowing zip truncates silently. Not knowing about zip_longest.Follow-up:- “How would you zip three lists together but raise an error if they have different lengths?”
- “What does
zip(*matrix)do and why is it useful?” - “Are
enumerateandziplazy or eager? What are the memory implications?”
86. range() function
86. range() function
range() generates a sequence of numbers lazily. In Python 3, it returns a range object (not a list), which is a lazy, immutable sequence that computes values on demand.Syntax: range(stop), range(start, stop), range(start, stop, step)range is special (not just a generator):- O(1) membership testing:
999_999 in range(1_000_000)is instant — it computes arithmetically, doesn’t iterate. - O(1) length:
len(range(1_000_000_000))is instant. - O(1) indexing:
range(1_000_000)[999_999]is instant. - Hashable and comparable: Two ranges are equal if they produce the same sequence:
range(0, 10, 2) == range(0, 10, 2)isTrue.
range is lazy and supports O(1) membership testing (not a generator that must iterate).Red flag answer: Saying range creates a list. Not knowing that in checks are O(1) for range. Using range(len(...)) instead of enumerate.Follow-up:- “How does
rangeachieve O(1) membership testing?” - “What is the difference between
rangein Python 2 vs Python 3?” - “Can you create a
range-like class for floats? What challenges would you face?”
100. PEP 8
100. PEP 8
- Indentation: 4 spaces (never tabs). This is non-negotiable in the Python community.
- Line length: 79 characters for code, 72 for docstrings/comments. Many teams extend to 88 (
blackdefault) or 100 in practice. - Blank lines: 2 between top-level functions/classes, 1 between methods inside a class.
- Imports: Always at the top of the file, grouped and ordered:
- Standard library (
import os,import sys) - Third-party (
import requests,import numpy) - Local/project (
from myapp import utils)
- Standard library (
- Naming conventions: (See Question 6 for full details.)
- Whitespace: One space around operators (
x = 1 + 2), no space inside brackets (func(arg)notfunc( arg )). - Comparisons: Use
is/is notforNone/True/False. Useif items:instead ofif len(items) > 0:.
ruff: Modern, Rust-based linter+formatter. Replacesflake8,isort,pycodestyle,pyflakes— all in one tool, 10-100x faster. Rapidly becoming the standard.black: Opinionated auto-formatter. “Any color you like, as long as it’s black.” Eliminates all formatting debates.mypy/pyright: Static type checkers (not PEP 8, but same category of code quality).- Pre-commit hooks: Run
ruffandblackautomatically before every commit. This is the real enforcement mechanism — not code review.
black/ruff to settle it automatically.Follow-up:- “How do you set up automated PEP 8 enforcement in a CI/CD pipeline?”
- “When is it acceptable to violate PEP 8?”
- “What is the difference between
ruff,flake8, andblack? How do they complement each other?”
101. Context Managers and the 'with' Statement
101. Context Managers and the 'with' Statement
__enter__ / __exit__ protocol.The problem they solve:- File handles, database connections, network sockets
- Acquiring/releasing locks:
with threading.Lock(): - Temporary directory:
with tempfile.TemporaryDirectory() as tmp: - Changing directory:
with contextlib.chdir('/tmp'): - Suppressing exceptions:
with contextlib.suppress(FileNotFoundError): - Redirecting stdout:
with contextlib.redirect_stdout(f):
__exit__ parameters explained:
__exit__(self, exc_type, exc_val, exc_tb) — if no exception occurred, all three are None. If an exception occurred, they contain the exception info. Returning True suppresses the exception (use sparingly).What interviewers are really testing: Whether you use with by default for resource management, whether you can build custom context managers, and whether you know the contextlib shortcuts.Red flag answer: Not using with for file operations. Not knowing how to write a custom context manager. Not knowing about contextlib.contextmanager.Follow-up:- “What happens if
__enter__raises an exception? Does__exit__still run?” - “How would you create an async context manager?”
- “When would you return
Truefrom__exit__to suppress an exception?”
102. Python's Memory Management and Garbage Collection
102. Python's Memory Management and Garbage Collection
- Reference counting (primary): Every object has a reference count. When it drops to zero, the object is immediately deallocated. This handles ~95% of garbage collection.
- Cyclic garbage collector (secondary): Handles circular references that reference counting can’t:
- Small object allocator (pymalloc): Objects under 512 bytes use Python’s internal allocator, avoiding expensive
mallocsystem calls. - Free lists: CPython caches recently deallocated objects (
int,float,tuple,list,dict) for reuse. - Interning: Small integers (-5 to 256) and certain strings are cached as singletons.
tracemalloc (stdlib), objgraph (visualize references), memory_profiler (@profile decorator), pympler (detailed object tracking).Common memory leak patterns:- Growing caches without eviction (use
functools.lru_cachewithmaxsize) - Circular references involving
__del__methods (GC can’t collect these before Python 3.4) - Closures capturing large objects unintentionally
- Appending to global lists in long-running processes
- “How would you debug a Python service that’s slowly leaking 100MB/hour?”
- “What is the
__slots__optimization and how does it affect memory usage?” - “How does
gc.disable()affect your application? When might you want to do this?”
103. Type Hints and Static Analysis
103. Type Hints and Static Analysis
mypy and pyright.- Catch bugs before runtime:
mypyfinds type mismatches, None-safety issues, and incorrect function calls at CI time. - Self-documenting code:
def fetch(url: str, timeout: float = 30.0) -> Responsetells you everything. - IDE intelligence: Autocomplete, refactoring, and navigation all improve dramatically.
- API contracts: When multiple teams work on a codebase, types serve as machine-checkable documentation.
mypy: The original type checker, strict and well-established.pyright(Microsoft): Faster, powers VS Code Pylance. Stricter than mypy by default.Pydantic: Runtime type validation (for API inputs, configs). Different from mypy (compile-time) vs Pydantic (runtime).
Protocol for duck typing.Red flag answer: Thinking type hints are enforced at runtime. Not knowing any type checking tools. Using Any everywhere to “satisfy” the type checker.Follow-up:- “What is the difference between
mypyandPydantic? When do you need each?” - “How do you handle gradual typing when adding type hints to a large existing codebase?”
- “What is
Protocoland how does it preserve Python’s duck typing philosophy while adding type safety?”
104. Dataclasses vs NamedTuples vs Regular Classes
104. Dataclasses vs NamedTuples vs Regular Classes
dataclass (Python 3.7+) — the modern default:NamedTuple — when you want immutability:| Feature | dataclass | NamedTuple | Regular class |
|---|---|---|---|
| Mutable by default | Yes | No (immutable) | Yes |
Auto __init__ | Yes | Yes | No (write manually) |
Auto __repr__ | Yes | Yes | No |
Auto __eq__ | Yes (value-based) | Yes (value-based) | No (identity-based) |
| Hashable | Only if frozen=True | Yes (immutable) | Only if __hash__ defined |
| Inheritance | Full support | Limited | Full support |
Memory with __slots__ | slots=True (3.10+) | Automatic | Manual |
| Tuple unpacking | No | Yes | No |
| Performance | Good | Excellent (C-backed) | Depends |
dataclass: Default choice for most data-holding classes. Mutable, feature-rich, familiar.NamedTuple: When immutability is important (configs, records, API responses), or when you need tuple interop.- Regular class: When you need complex
__init__logic, metaclasses, or heavy customization. attrs(third-party): When you need features beyonddataclass(validators, converters, more control). Many large codebases useattrsoverdataclass.
dataclass instead of writing boilerplate, whether you know when immutability matters, and whether you know the field(default_factory=...) pattern for mutable defaults.Red flag answer: Manually writing __init__, __repr__, __eq__ for simple data classes. Not knowing about frozen=True. Using a regular class when dataclass or NamedTuple would be cleaner.Follow-up:- “How does
dataclass(frozen=True)enforce immutability? Can it be bypassed?” - “What does
__slots__do and why doesdataclass(slots=True)exist?” - “When would you choose
attrsoverdataclass?”
105. Async/Await and Asyncio
105. Async/Await and Asyncio
asyncio is Python’s built-in framework for concurrent I/O-bound programming using a single-threaded event loop. It enables handling thousands of concurrent network connections without the overhead of OS threads.Core concepts:- When a coroutine hits
await, it yields control back to the event loop - The event loop runs other ready coroutines while the awaited operation completes
- When the I/O completes, the event loop resumes the paused coroutine
- Only one coroutine runs at a time (single-threaded) — concurrency, not parallelism
- asyncio: Many concurrent I/O operations (web scraping, API calls, chat servers). Best when you have 100+ concurrent tasks.
- threading: Simpler I/O concurrency with existing sync libraries that can’t be made async.
- multiprocessing: CPU-bound work that needs true parallelism.
- Blocking the event loop: Calling
time.sleep()(blocking) instead ofawait asyncio.sleep()(non-blocking) freezes ALL coroutines. - Forgetting to
await:result = fetch_data("url")returns a coroutine object, not the result. - Mixing sync and async: Use
loop.run_in_executor()to call sync functions from async code.
time.sleep in async code. Not knowing the difference between asyncio.gather and sequential await.Follow-up:- “What happens if you accidentally call a blocking function inside an async coroutine?”
- “How does
asyncio.gatherdiffer fromasyncio.TaskGroup(Python 3.11+)?” - “How would you add asyncio to an existing synchronous Flask application?”
106. Python's Descriptor Protocol
106. Python's Descriptor Protocol
@property, @classmethod, @staticmethod, __slots__, and Python’s bound method system. Understanding descriptors means understanding how Python’s attribute access actually works.What is a descriptor?
Any object that defines __get__, __set__, or __delete__ is a descriptor. When placed on a class, it intercepts attribute access on instances of that class.- Data descriptor: Defines
__set__and/or__delete__. Takes priority over instance__dict__. - Non-data descriptor: Only defines
__get__. Instance__dict__takes priority.
@property (data descriptor) can intercept assignment, while regular methods (non-data descriptors) can be overridden by instance attributes.The attribute lookup chain:- Data descriptors on the class (e.g.,
@property) - Instance
__dict__ - Non-data descriptors on the class (e.g., methods)
__getattr__(fallback)
@property and Python’s method binding. This is a staff-level question — most seniors don’t fully understand descriptors.Red flag answer: Never having heard of descriptors. Not understanding why @property works.Follow-up:- “How does Python’s method binding work? Why is
obj.methoda bound method butClass.methodis a function?” - “What is the difference between a data descriptor and a non-data descriptor in attribute lookup priority?”
- “How would you implement a caching descriptor similar to
functools.cached_property?”
107. Common Python Anti-Patterns and How to Fix Them
107. Common Python Anti-Patterns and How to Fix Them
type() for type checking:is for value comparison:enumerate:type() instead of isinstance().Follow-up:- “You’re reviewing a PR and see
except Exception: pass. What do you say?” - “Why is string concatenation in a loop O(n^2)? What does CPython do internally?”
- “What other anti-patterns have you seen in production Python code?”