Skip to content

Cookies on Jobverse

We use cookies that are needed to sign you in and keep the site working. With your permission we’d also use an analytics cookie to count job views, and Google advertising cookies to show ads. The site works the same whatever you choose. Read the Cookie Policy.

Jobverse
Sign in

Python Backend Developer Top 100 Interview Questions

100 Python backend developer interview questions with structured answers, comparison tables, flow diagrams, worked calculations and runnable code. Covers the language core, concurrency and asyncio, web request handling, databases, caching, testing, security, performance and system design, from the basics to production scenarios.

100 questions

Questions 1-20 of 100

  1. Q1.What does a Python backend developer own beyond writing API endpoints?

    A Python backend developer owns the whole path from a request to stored data and back, including how that path behaves under load and failure. Endpoints are only the visible part of the job.

    • Domain logic: turning business rules into clear, tested code that does not depend on the web layer.
    • Data access: schemas, queries, transactions and migrations that keep data correct as the system changes.
    • Integration: calling other services and queues with timeouts, retries and clear error handling.
    • Performance: knowing where the interpreter is slow, when to use concurrency, and how to measure before optimizing.
    • Security: validating input, protecting secrets, binding query parameters and keeping dependencies patched.
    • Operations: logging, metrics, health checks, graceful shutdown and packaging the service so it runs the same everywhere.

    Good Python backend engineers also make the codebase easy for the next person: consistent structure, type hints that tools can check, and tests that run quickly. The language makes it easy to write code fast, so discipline about structure matters more, not less.

  2. Q2.What happens when the Python interpreter runs a module?

    The interpreter compiles the source into bytecode, then a virtual machine executes that bytecode one instruction at a time. Python code is never run directly as text.

    flowchart LR
      A["Source file"] --> B["Parse into syntax tree"]
      B --> C["Compile to bytecode"]
      C --> D["Cache bytecode for imported modules"]
      C --> E["Virtual machine executes bytecode"]
      E --> F["Objects and results in memory"]
    1. Parsing: the source becomes an abstract syntax tree, and syntax errors are reported at this stage.
    2. Compilation: the tree becomes bytecode for each function and the module body.
    3. Caching: for imported modules, the reference interpreter writes the bytecode to a cache folder, so later imports skip compilation when the source has not changed.
    4. Execution: the evaluation loop runs the bytecode, creating objects, calling functions and handling exceptions.

    Why it matters in practice:

    • Importing a module runs its top-level code, so slow work at import time slows every process start.
    • Recent versions specialize frequently executed bytecode for speed, but heavy numeric loops still belong in native code or optimized libraries.
  3. Q3.How does Python pass arguments to functions?

    Python passes references to objects, sometimes called pass-by-object-reference or call by sharing. The function gets a new name bound to the same object the caller passed.

    • Mutating the object inside the function, such as appending to a list, is visible to the caller, because both names point to one object.
    • Rebinding the parameter to a new object, such as tags = [], only changes the local name; the caller's variable is untouched.
    • Immutable objects such as integers, strings and tuples cannot be changed in place, so a function can never alter the caller's value through them.

    Practical rules for backend code:

    • Do not mutate input arguments unless the function's name and documentation say so; return a new value instead.
    • Copy input collections before changing them when the caller may reuse them.
    • Be careful with objects shared across requests, such as module-level lists or dictionaries, since any request can change them for all others.

    Example

    The list is changed by mutation, while rebinding inside the second function has no effect on the caller.

    def add_tag(tags):
        tags.append("new")
    
    def replace_tags(tags):
        tags = ["other"]
    
    items = ["a"]
    add_tag(items)
    replace_tags(items)
    print(items)  # ['a', 'new']
  4. Q4.Why is a mutable default argument a common bug, and how do you fix it?

    Default argument values are evaluated once, when the function is defined, not on every call. A mutable default such as a list or dictionary is therefore shared by every call that does not pass that argument.

    • Each call that relies on the default mutates the same object, so data leaks from one call into the next.
    • In a web service, this can mix data between requests from different users, which is both a bug and a privacy risk.
    • The fix is to use None as the default and create a fresh object inside the function.

    Related traps:

    • Defaults computed from the current time, such as a timestamp, are also fixed at definition time.
    • Class attributes that are mutable are shared by all instances in the same way.
    • Static analysis tools and linters flag mutable defaults, so turn that rule on in the project.

    Example

    The first function shares one list across calls; the fixed version creates a new list each time.

    def add_item_bad(item, bucket=[]):
        bucket.append(item)
        return bucket
    
    def add_item(item, bucket=None):
        if bucket is None:
            bucket = []
        bucket.append(item)
        return bucket
    
    print(add_item_bad("a"))  # ['a']
    print(add_item_bad("b"))  # ['a', 'b']
    print(add_item("a"))  # ['a']
    print(add_item("b"))  # ['b']
  5. Q5.How do lists, tuples, sets and dictionaries differ, and when do you use each?

    They are the four core built-in collections, and they differ in ordering, mutability and lookup speed. Choosing the right one is often the simplest performance and correctness win in Python code.

    ListTupleSetDictionary
    Ordered, mutable sequenceOrdered, immutable sequenceUnordered group of unique itemsKey-value mapping that keeps insertion order
    Allows duplicatesAllows duplicatesDrops duplicatesKeys are unique
    Membership check scans every itemMembership check scans every itemAverage constant-time membershipAverage constant-time lookup by key
    Use for ordered items you changeUse for fixed records and as dictionary keysUse for uniqueness and fast membershipUse for lookups by key

    When to use which:

    • Use a list for ordered data you append to or sort, such as rows returned from a query.
    • Use a tuple for a fixed group of values, such as a composite key, because it is hashable when its items are.
    • Use a set to remove duplicates or to test membership many times.
    • Use a dictionary to index records by an identifier instead of searching a list repeatedly.
  6. Q6.How does a dictionary look up a key, and what makes an object hashable?

    A dictionary is a hash table: it computes the key's hash, uses it to find a slot, then confirms the match with equality. That is why lookups take constant time on average.

    1. Hash: Python calls the key's __hash__ method to get an integer.
    2. Probe: the hash picks a slot; if the slot holds a different key, the table probes further slots.
    3. Compare: keys with the same hash are compared with __eq__ to find the exact match.

    What makes an object hashable:

    • It has a hash that never changes while it is used as a key.
    • Objects that compare equal must have equal hashes.
    • Built-in immutable types such as strings, numbers and tuples of hashable items qualify; lists, sets and dictionaries do not.

    Practical consequences:

    • If you define __eq__ on a class, also define __hash__, or Python makes instances unhashable.
    • Changing an object's fields after using it as a key can make it impossible to find again.
    • Many keys with colliding hashes degrade lookups toward linear time, which is why hash randomization protects against attackers who choose keys.
  7. Q7.What is the difference between `is` and `==` in Python?

    == checks equality, meaning whether two objects have the same value as defined by their __eq__ method. is checks identity, meaning whether two names refer to the very same object in memory.

    ==is
    Checks equality through the __eq__ methodChecks identity: the very same object
    [1, 2] == [1, 2] is True[1, 2] is [1, 2] is False
    Use for numbers, strings and collectionsUse for None, True, False and your own sentinel objects

    Why is sometimes appears to work for values:

    • The reference interpreter caches some small integers and may reuse some short strings, so a is b can be true for them by accident.
    • That behavior is an implementation detail and changes between versions and situations, so code that relies on it is a bug waiting to happen.

    Interview tip: explain why x is None is preferred over x == None; a class can override __eq__ to return anything, but identity with the single None object cannot be faked.

  8. Q8.What is the difference between a shallow copy and a deep copy?

    A shallow copy creates a new outer container but keeps references to the same inner objects. A deep copy recursively copies every nested object, so the copy shares nothing mutable with the original.

    Shallow copyDeep copy
    New outer container, same inner objectsRecursively copies every nested object
    list(items), dict(config), slicing or copy.copycopy.deepcopy, which handles shared and circular references
    Best when inner objects are immutable, such as stringsBest when you will modify nested structures

    Pitfalls:

    • Changing a nested list through a shallow copy also changes it in the original, which surprises many developers.
    • A per-request copy of a configuration template needs a deep copy if handlers change nested values.
    • Deep copies are slower and use more memory, so for large structures consider building a new object from only the fields you need.

    Example

    Appending through the shallow copy changes the original's inner list, while the deep copy stays independent.

    import copy
    
    config = {"retries": 3, "hosts": ["a", "b"]}
    shallow = copy.copy(config)
    deep = copy.deepcopy(config)
    
    shallow["hosts"].append("c")
    shallow["retries"] = 5
    
    print(config["hosts"])  # ['a', 'b', 'c']
    print(config["retries"])  # 3
    print(deep["hosts"])  # ['a', 'b']
  9. Q9.How do generators work, and why are they useful in backend code?

    A generator is a function that uses yield to produce values one at a time. Calling it returns an iterator, and each next call runs the function until the next yield, then pauses with its local state saved.

    • Memory: only the current item exists in memory, so you can process files, query results or streams far larger than available RAM.
    • Pipelines: small generators can be chained, each doing one step such as parsing, filtering or batching.
    • Laziness: work happens only when a consumer asks for the next item, and stops early if the consumer stops.

    Things to watch:

    • A generator can be consumed only once; iterating it again yields nothing.
    • Exceptions inside a generator surface when the consumer asks for the item that failed.
    • Resources opened inside a generator, such as a file, should be managed with with, so they close even if the consumer stops early.

    Example

    Two chained generators filter error lines without ever building an intermediate list.

    def stripped(lines):
        for line in lines:
            yield line.strip()
    
    def errors_only(lines):
        for line in lines:
            if " ERROR " in line:
                yield line
    
    log = ["10:00 INFO start\n", "10:01 ERROR db timeout\n", "10:02 INFO ok\n", "10:03 ERROR retry failed\n"]
    pipeline = errors_only(stripped(log))
    
    print(next(pipeline))  # 10:01 ERROR db timeout
    print(list(pipeline))  # ['10:03 ERROR retry failed']
  10. Q10.How do list comprehensions and generator expressions differ?

    They share almost the same syntax, square brackets for a list comprehension and parentheses for a generator expression, but they evaluate differently. One builds the whole result immediately; the other produces items on demand.

    List comprehensionGenerator expression
    Builds the whole list immediatelyProduces items one at a time, when asked
    Memory grows with the size of the resultMemory stays small and constant
    Can be iterated many times and indexedCan be iterated only once
    Best when you need all results or reuse themBest for streaming large data into one consumer

    When to use which:

    • Use a generator expression when passing the result straight to a function that consumes it once, such as sum(order.total for order in orders).
    • Use a list comprehension when you need the length, indexing, sorting or several passes over the data.
    • Keep both short and readable; when the logic needs several conditions or nested loops, a normal loop or a named generator function is clearer.
  11. Q11.How do decorators work, and how would you write a retry decorator?

    A decorator is a function that takes a function and returns a replacement, usually a wrapper that adds behavior before or after calling the original. The @name syntax is shorthand for func = name(func).

    • Wrapping: the wrapper accepts *args and **kwargs so it works for any signature.
    • Metadata: functools.wraps copies the original name and docstring onto the wrapper, which keeps logs, debugging and documentation correct.
    • Parameters: a decorator that takes its own arguments, such as @retry(times=3), is a function that returns the actual decorator.

    Common backend uses: retries, timing, caching, access checks and registering route handlers.

    Rules for a retry decorator:

    • Retry only errors that are likely to be temporary, such as connection failures, never validation errors.
    • Wait longer after each attempt and cap the number of attempts.
    • Re-raise the last error so callers still see the failure.
    • Retry only operations that are safe to repeat.

    Example

    The decorator retries only connection errors, so the flaky call succeeds on its third attempt.

    import functools
    import time
    
    def retry(times, delay=0.0, exceptions=(Exception,)):
        def decorator(func):
            @functools.wraps(func)
            def wrapper(*args, **kwargs):
                for attempt in range(1, times + 1):
                    try:
                        return func(*args, **kwargs)
                    except exceptions:
                        if attempt == times:
                            raise
                        time.sleep(delay * 2 ** (attempt - 1))
            return wrapper
        return decorator
    
    calls = {"count": 0}
    
    @retry(times=3, exceptions=(ConnectionError,))
    def fetch_profile(user_id):
        calls["count"] += 1
        if calls["count"] < 3:
            raise ConnectionError("temporary failure")
        return {"id": user_id, "name": "Asha"}
    
    print(fetch_profile(7))  # {'id': 7, 'name': 'Asha'}
    print(calls["count"])  # 3
    print(fetch_profile.__name__)  # fetch_profile
  12. Q12.How do context managers work, and when would you write your own?

    A context manager defines setup and cleanup around a block of code used with with. Python calls __enter__ at the start and __exit__ at the end, even when the block raises an exception.

    sequenceDiagram
      participant C as Calling code
      participant M as Context manager
      participant B as With block
      C->>M: __enter__
      M-->>C: Resource
      C->>B: Run the block
      B-->>C: Returns or raises
      C->>M: __exit__ with the exception details, or None
      M-->>C: Cleanup done, true only to suppress the error
    • Guaranteed cleanup: files, locks, database transactions and network connections are released however the block exits.
    • Exception handling: __exit__ receives the exception and can log it, roll back a transaction or, rarely, suppress it by returning true.
    • Simple version: the contextlib.contextmanager decorator turns a generator with one yield into a context manager, with cleanup in a finally block.

    When to write your own:

    • Pairing operations that must always happen together, such as acquire and release or begin and commit.
    • Temporarily changing state, such as a setting in a test, and restoring it afterwards.
    • Timing a block of code for metrics.

    For several resources at once, contextlib.ExitStack manages a variable number of them and closes each one in reverse order.

    Example

    The timing context manager records a duration even though the block raises an error.

    import time
    from contextlib import contextmanager
    
    @contextmanager
    def timed(label, results):
        start = time.perf_counter()
        try:
            yield
        finally:
            results[label] = time.perf_counter() - start
    
    results = {}
    try:
        with timed("query", results):
            raise TimeoutError("database timeout")
    except TimeoutError as error:
        print("caught:", error)  # caught: database timeout
    
    print("query" in results)  # True
  13. Q13.How does variable scope work in Python, and what do `global` and `nonlocal` do?

    Python resolves a name by searching four scopes in order: local, enclosing function, global module and built-in. A name assigned anywhere inside a function is local to that function unless declared otherwise.

    • global lets a function assign to a module-level name. In backend code it usually signals shared mutable state, which is risky across requests and threads.
    • nonlocal lets an inner function assign to a variable of its enclosing function, which is how closures keep counters or caches.
    • Closures capture variables, not values, so an inner function sees the variable's value at the time it runs.

    A classic trap: functions created in a loop all see the loop variable's final value, because they share one variable. Binding the current value as a default argument, or using a factory function, captures it at creation time.

    Assigning to a name before reading it in the same function makes it local everywhere in that function. Reading it first then raises an UnboundLocalError.

    Example

    The first list of functions shares one loop variable; binding a default captures each value, and nonlocal updates the enclosing counter.

    handlers = [lambda: i for i in range(3)]
    print([handler() for handler in handlers])  # [2, 2, 2]
    
    handlers = [lambda i=i: i for i in range(3)]
    print([handler() for handler in handlers])  # [0, 1, 2]
    
    def counter():
        count = 0
        def increment():
            nonlocal count
            count += 1
            return count
        return increment
    
    next_id = counter()
    next_id()
    print(next_id())  # 2
  14. Q14.How should a Python service raise, chain and handle exceptions?

    Raise specific, meaningful exceptions where an error is detected, and handle them only where you can do something useful, usually at the boundary that turns them into a response. Swallowing errors silently is worse than crashing.

    • A small hierarchy: define a base error for the service and subclasses such as NotFoundError and ConflictError, so callers can catch by category.
    • Chaining: use raise DomainError(...) from error when translating a low-level failure, so the original cause stays in the traceback.
    • Catch narrowly: catch the exceptions you expect, never a bare except:, which also catches interrupts and system exits.
    • Log once: log the full traceback at the boundary that handles the error, not at every level it passes through.
    • Clean up: use with or finally for resources rather than relying on exception handlers.

    Python favors EAFP, "easier to ask forgiveness than permission": try the operation and handle the failure, instead of checking every condition first. This also avoids race conditions between a check and the action.

    When several concurrent tasks fail together, recent versions group them in an ExceptionGroup, handled with except*.

  15. Q15.When do you use a dataclass, a named tuple, a typed dictionary or a plain class?

    All four describe structured data, but they differ in mutability, behavior and whether the structure exists at runtime or only for type checkers.

    DataclassNamed tupleTyped dictionaryPlain class
    Generates init, repr and comparison methods from annotated fieldsAn immutable tuple with named fieldsA dictionary shape described for type checkersFull manual control over behavior
    Mutable by default, or frozen on requestImmutable, and unpackable like a tupleAn ordinary mutable dictionary at runtimeMutable unless you prevent it
    No runtime type checking of fieldsNo runtime type checkingNo runtime checking at allValidates only what you code
    Best for internal domain objectsBest for small fixed recordsBest for JSON-like payloadsBest for rich behavior and invariants

    When to use which:

    • Use a dataclass as the default for internal records; mark it frozen when instances should not change.
    • Use a named tuple when you need tuple behavior, such as unpacking or use as a dictionary key.
    • Use a typed dictionary when the data really is a dictionary, such as a decoded request body.
    • Use a plain class when the object enforces rules in its methods.

    Remember that type annotations are not validation; incoming data still needs explicit checks.

  16. Q16.What do type hints do at runtime, and how do teams use them?

    At runtime, type hints do almost nothing: Python stores them as annotations but does not enforce them. Their value comes from tools that read them, such as static type checkers, editors and libraries that build validation or serialization from them.

    • Static checking: a type checker in the build catches wrong argument types, missing None handling and typos before the code runs.
    • Documentation: signatures show what a function expects and returns, which helps every reader.
    • Structural typing: a Protocol describes the methods an object needs, so any matching class fits without inheritance.
    • Generics: type variables describe relationships, such as a function that returns the same type it received.

    How teams adopt them:

    1. Type public functions and new code first, rather than the whole codebase at once.
    2. Run the type checker in the pipeline, starting lenient and tightening over time.
    3. Validate external input explicitly, because annotations alone never reject bad data at runtime.

    Avoid Any as a habit; it switches off checking for everything it touches.

  17. Q17.How do special methods let your classes work with built-in operations?

    Special methods, often called dunder methods for their double underscores, are hooks that Python calls for built-in syntax and functions. Implementing them makes your objects behave like built-in types.

    • Representation: __repr__ gives an unambiguous developer view used in logs and debuggers; __str__ gives a user-facing one.
    • Comparison and hashing: __eq__ defines equality, and __hash__ must be consistent with it for use in sets and dictionary keys.
    • Operators: __add__, __lt__ and friends let a + b and sorting work.
    • Containers: __len__, __iter__, __contains__ and __getitem__ support len, loops, in and indexing.
    • Context management: __enter__ and __exit__ support with.

    Rules that avoid subtle bugs:

    • Return NotImplemented from a comparison when the other object is of an unsupported type, so Python can try the reverse operation.
    • Keep the fields used by __hash__ immutable.
    • Always define a useful __repr__; it pays off in every log line.

    Example

    A small money type supports addition, equality, hashing and a readable representation.

    class Money:
        def __init__(self, cents, currency):
            self.cents = cents
            self.currency = currency
    
        def __repr__(self):
            return f"Money({self.cents}, {self.currency!r})"
    
        def __eq__(self, other):
            if not isinstance(other, Money):
                return NotImplemented
            return (self.cents, self.currency) == (other.cents, other.currency)
    
        def __hash__(self):
            return hash((self.cents, self.currency))
    
        def __add__(self, other):
            if self.currency != other.currency:
                raise ValueError("currency mismatch")
            return Money(self.cents + other.cents, self.currency)
    
    total = Money(250, "INR") + Money(100, "INR")
    print(total)  # Money(350, 'INR')
    print(total == Money(350, "INR"))  # True
    print(len({Money(1, "INR"), Money(1, "INR")}))  # 1
  18. Q18.How does method resolution order work with multiple inheritance?

    The method resolution order is the sequence of classes Python searches when looking up an attribute. Python computes it with an algorithm called C3 linearization, which keeps every class before its parents and preserves the order you list the bases in.

    • ClassName.__mro__ shows the order explicitly.
    • super() does not mean "the parent"; it means the next class in the resolution order of the actual object.
    • That is what makes cooperative multiple inheritance work: each class calls super(), and every class in the chain runs exactly once.

    Practical guidance:

    • Use multiple inheritance mainly for small mixins that add one behavior, such as auditing or serialization.
    • Make every class in a cooperative chain accept and pass on **kwargs, so constructors with different arguments still work.
    • Prefer composition, holding a collaborator as an attribute, when the classes are not genuinely variations of one type.

    If Python cannot build a consistent order, it raises a TypeError when the class is defined.

    Example

    Each save call passes along the resolution order, so all four classes run once and in a predictable sequence.

    class Base:
        def save(self):
            return ["Base"]
    
    class Audited(Base):
        def save(self):
            return ["Audited"] + super().save()
    
    class Cached(Base):
        def save(self):
            return ["Cached"] + super().save()
    
    class Repository(Audited, Cached):
        def save(self):
            return ["Repository"] + super().save()
    
    print([cls.__name__ for cls in Repository.__mro__])  # ['Repository', 'Audited', 'Cached', 'Base', 'object']
    print(Repository().save())  # ['Repository', 'Audited', 'Cached', 'Base']
  19. Q19.When would you use an abstract base class instead of a protocol?

    Both define an interface, but an abstract base class works through inheritance and runtime enforcement, while a protocol works through structural typing: any class with the right methods qualifies.

    Abstract base classProtocol
    Subclasses must inherit from it explicitlyAny class with matching methods fits, without inheritance
    Instantiating a subclass with missing abstract methods raises an errorChecked by static type checkers; runtime checks only when marked runtime-checkable
    Can share implementation through concrete methodsDescribes the required shape only
    Fits when you own the class hierarchyFits for third-party classes and loosely coupled code

    When to use which:

    • Use an abstract base class for a plugin system you control, where shared helper methods and a hard runtime check are valuable.
    • Use a protocol for dependencies such as "anything with a send(message) method", so tests can pass simple fakes and existing classes fit without changes.

    A runtime-checkable protocol only checks that the methods exist, not their signatures, so do not rely on it as validation.

  20. Q20.How does Python manage memory?

    The reference interpreter frees most objects through reference counting, and a separate cyclic garbage collector cleans up groups of objects that refer to each other. You rarely free memory yourself, but you can easily keep objects alive by accident.

    • Reference counting: every object tracks how many references point to it, and it is freed as soon as the count reaches zero.
    • Cycle collection: objects that reference each other, such as a parent and child pointing to one another, never reach zero on their own, so the collector finds and frees unreachable cycles periodically.
    • Memory pools: small objects come from internal pools, so freed memory is often reused by Python rather than returned to the operating system immediately.

    How leaks happen in backend services:

    • Caches and module-level collections that grow without limits.
    • References held in closures, registered callbacks or long-lived task objects.
    • Large objects kept alive by a traceback stored in a log record or variable.

    The standard tracemalloc module compares memory snapshots to show which lines allocate the memory that keeps growing.