460 Python Developer Interview Questions & Answers

154 top • 31 Amazon • 49 Google • 44 Netflix • 48 Meta • 41 NVIDIA • 47 Apple • 46 Microsoft

Python Developer icon

Questions with Detailed ExplanationsWith Detailed Explanations

(Last Updated: September 3, 2026)

61. How do decorators work in Python?Language SpecificMedium

Question Details

Explain that functions are first-class objects, how a decorator receives and replaces a callable, how decorator syntax is evaluated, how to preserve metadata, and how to write a decorator that accepts arguments.

Short Interview Answer (30-60 seconds)

A decorator receives a callable and returns the object that will replace it. Python applies the decorator when it executes the function definition. The at syntax is equivalent to assigning the result of the decorator back to the function name. A wrapper can run logic before or after the original call. In production code, I normally use functools.wraps so tools can still find the original name, documentation, and wrapped function.

Detailed Explanation

See the Code while reading this explanation.

A decorator changes what a function name refers to. Python can do this because functions are first class objects. They can be passed to another function and returned as values.

Useful Questions to Ask the Interviewer
  1. Should I focus on Python language behavior, or also explain the runtime and standard library?
  2. Which Python version and execution environment should I assume?
  3. Would you like a small code example together with production tradeoffs and edge cases?

When Python executes a decorated function definition, it creates the original function first. It then evaluates the decorator expression, passes the function to the decorator, and assigns the returned object to the original name. Therefore, applying @logger to process is equivalent to process = logger(process). Decoration happens when that definition is executed, which is often during module import, not on every later call.

A common decorator returns a wrapper function. The wrapper runs extra logic, calls the original function, and returns its result. It often accepts *args and **kwargs so it can forward different arguments.

A decorator with arguments needs three levels. The outer function receives configuration, the next function receives the target callable, and the wrapper handles each call.

functools.wraps copies useful metadata and sets __wrapped__. It does not remove the extra call cost. A closure can also keep captured objects alive while the decorated function remains reachable.

How do decorators work in Python? diagram
Example

The example uses a decorator factory named log_calls. The outer function receives the label argument. It returns the real decorator, which receives the target function. The decorator returns a wrapper that accepts any positional and keyword arguments. functools.wraps preserves important metadata and adds a __wrapped__ reference to the original function. Each call prints the configured label and function name, calls the original function once, and returns its result unchanged.

Code
from functools import wraps
from typing import Any, Callable


def log_calls(label: str) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
    """Create a decorator that logs a label before each function call."""

    # This function receives the target function.
    def decorator(func: Callable[..., Any]) -> Callable[..., Any]:

        # wraps copies useful metadata and sets __wrapped__.
        @wraps(func)
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            # Run the added behavior before the original function.
            print(f"{label}: calling {func.__name__}")

            # Call the original function once and return its result unchanged.
            return func(*args, **kwargs)

        # The returned wrapper replaces the original function name.
        return wrapper

    # Return the real decorator after receiving its configuration.
    return decorator


@log_calls("INFO")
def add(left: int, right: int) -> int:
    """Return the sum of two integers."""
    return left + right


result = add(2, 3)
print(result)
print(add.__name__)
print(add.__doc__)
print(add.__wrapped__(4, 5))
Where it is used

Decorators are useful when the same small behavior must be applied to many callables. Production examples include permission checks, logging, timing, caching, input validation, retry policies, route registration, and test markers. They are a good choice when the added behavior is reusable and clearly connected to the function. They are a poor choice when they hide major control flow, silently change return values, or make failures difficult to trace.

Why Interviewers Ask This

Interviewers ask this question to check whether a candidate understands that functions are objects in Python and can reason about function creation, callable replacement, closures, metadata, and decorator arguments. It also tests whether the candidate can use decorators without hiding important behavior or creating difficult debugging and typing problems.

Common interview mistakes

Common mistakes include calling the target function while creating the decorator instead of inside the wrapper, forgetting to return the wrapper, and forgetting to return the original result. A wrapper with fixed parameters may fail for functions with different call signatures, so forwarding *args and **kwargs is common. Omitting functools.wraps causes misleading names and documentation. Another mistake is assuming wraps gives the wrapper the exact runtime signature of the original function. It preserves metadata and sets __wrapped__, but the wrapper itself still uses its declared parameters. Decorators can also be applied in the wrong order because stacked decorators are applied from the bottom upward.

Interview tip

Begin with the replacement rule. Say that @decorator above a function is equivalent to assigning decorator(function) back to the same name. Then explain the wrapper, when decoration occurs, why functools.wraps matters, and why decorator arguments require one extra function level.

Interviewer may ask next
What happens when several decorators are stacked on one function?

Python applies stacked decorators from the bottom upward. For @outer above @inner, the result is function = outer(inner(function)). Calls then normally enter the outer wrapper first and continue inward. This matters because decorator order can change validation, caching, logging, exceptions, and returned values.

What performance and memory costs can a decorator add?

A wrapper adds at least one extra Python function call for each decorated call, plus the cost of its own work. Several stacked wrappers add several call layers. A closure also stores references to captured values such as the original function and configuration. Those objects can remain alive while the decorated callable remains reachable. The cost is often small, but it can matter for very frequently called functions or when a closure captures large objects.

62. How do closures work in Python?Language SpecificMedium

Question Details

Explain how an inner function retains access to names from an enclosing scope, how late binding affects captured loop variables, and when nonlocal is needed to rebind captured state.

Short Interview Answer (30-60 seconds)

A closure is an inner function that keeps access to names from an enclosing function even after the enclosing function has returned. Python keeps references to those captured names rather than copying their current values. This causes late binding, so functions created in a loop may all read the final loop value. I can save each current value with a default argument, and I use nonlocal only when the inner function must rebind a captured name.

Detailed Explanation

See the Code while reading this explanation.

A closure is created when an inner function refers to a name from an enclosing function scope and the inner function is returned or stored for later use. Python keeps the captured name in a closure cell, so the inner function can still access it after the outer call has finished.

Useful Questions to Ask the Interviewer
  1. Should I focus on Python language behavior, or also explain the runtime and standard library?
  2. Which Python version and execution environment should I assume?
  3. Would you like a small code example together with production tradeoffs and edge cases?

The captured object is not copied. The closure keeps a reference to it. Python also uses late binding for captured names. The value is normally read when the inner function runs, not when its function object is created. This matters in loops because several functions may share the same loop variable and later return its final value. A common fix is a default argument because its value is evaluated each time the def statement runs.

Reading a captured name needs no keyword. Mutating a captured mutable object also needs no nonlocal statement. However, assigning a different object to a captured name is rebinding, so nonlocal is required. It targets the nearest enclosing function scope and cannot target a global name.

Closures are useful for decorators, callbacks, factories, and small private state. Creating or calling one is normally constant time, apart from the work inside it. Captured references use memory and may keep large objects alive. For complex state, a class is often clearer.

How do closures work in Python? diagram
Example

The code demonstrates the three behaviors required by the question. make_multiplier captures factor and reads it after make_multiplier has returned. build_functions stores the current loop value in a default argument, so each returned function has its own saved value instead of reading one shared loop variable later. make_counter uses nonlocal because increment assigns a new integer object to count. The examples print 12, then the list 0, 1, 2, and then the counter values 1 and 2.

Code
def make_multiplier(factor):
    # This inner function captures factor from the enclosing scope.
    def multiply(number):
        # factor remains available after make_multiplier returns.
        return number * factor

    # Return the function object for later use.
    return multiply


def build_functions():
    functions = []

    for value in range(3):
        # This default value is evaluated when this def statement runs.
        # Each loop iteration therefore stores its own current value.
        def read_value(saved_value=value):
            return saved_value

        functions.append(read_value)

    return functions


def make_counter():
    count = 0

    def increment():
        # Assignment would otherwise make count local to increment.
        # nonlocal allows rebinding in the nearest enclosing function scope.
        nonlocal count
        count += 1
        return count

    return increment


# The closure keeps factor equal to 3.
triple = make_multiplier(3)
print(triple(4))

# Each function returns the value saved during its loop iteration.
readers = build_functions()
print([reader() for reader in readers])

# The closure keeps and updates count between calls.
counter = make_counter()
print(counter())
print(counter())
Where it is used

Closures are used in decorator factories, callback creation, event handlers, configured functions, and small stateful utilities. A retry decorator can capture a retry limit. A validation function can capture configuration. A counter can keep private state between calls. Closures work best when the captured state is small and the behavior has only a few operations.

Why Interviewers Ask This

Interviewers ask this question to test whether a candidate understands nested scopes, function objects, closure cells, late binding, and the nonlocal statement. It also shows whether the candidate can recognize subtle bugs in callbacks, decorators, and functions created inside loops.

Common interview mistakes

A common mistake is assuming that a closure stores an independent frozen copy of every captured value. It normally stores access to a closure cell, so late binding can make loop functions share the final loop value. Another mistake is assigning to a captured name without nonlocal. Python then treats that name as local to the inner function, which can cause UnboundLocalError if it is read before assignment. Developers also sometimes use nonlocal when only mutating a captured list or dictionary, even though rebinding is not occurring. A closure can also keep captured objects alive longer than expected, which matters when those objects use significant memory or hold external resources.

Interview tip

Begin with the main rule that a closure keeps access to an enclosing scope. Then explain that Python captures names through closure cells and uses late binding. Use a loop example to show the problem, explain the default argument fix, and finish by stating that nonlocal is needed for rebinding but not for reading or mutating a captured object.

Interviewer may ask next
Why do functions created in a loop often return the same final value?

They often return the same final value because Python uses late binding for the captured loop name. The functions share access to the same closure cell, and they read that cell when they are called. After the loop ends, the cell contains the final loop value. A default argument fixes this behavior by evaluating and storing the current value each time the def statement runs.

Do you need nonlocal to change a captured list?

No, nonlocal is not needed when the inner function only mutates the existing captured list, such as by calling append. The captured name still refers to the same list object. nonlocal is required only when the function assigns a different object to that name. This distinction matters because mutation changes an object, while rebinding changes which object the name refers to.

63. What are type hints in Python?Language SpecificEasy

Question Details

Define type hints as optional annotations that describe expected value types for readers, editors, linters, and static type checkers. Explain parameter, return, variable, collection, union, optional, protocol, and generic annotations, and make clear that normal Python execution does not automatically enforce most hints. Distinguish static checking from runtime validation.

Short Interview Answer (30-60 seconds)

Type hints are optional annotations that describe the types a function, variable, or collection is expected to use. They help readers, editors, linters, and static type checkers understand the code. Python normally does not enforce most type hints when the program runs, so a value with a different type can still reach the function. If runtime validation is required, the program needs separate validation logic or a library that performs it.

Detailed Explanation

Type hints are notes that describe what kind of value a part of a Python program is expected to receive, store, or return. They make code easier for people to understand and help development tools find some mistakes before the program runs. These notes can describe one value, a group of values, a value that may be missing, or several allowed kinds of values. The key point is that these notes usually guide people and tools. They do not normally stop a running Python program from receiving a different kind of value.

Useful Questions to Ask the Interviewer
  1. Should I focus only on normal Python behavior, or also discuss runtime validation tools?
  2. Would you like examples of modern annotation syntax?
What are type hints in Python? diagram
How to Explain It in an Interview

Type hints are optional annotations. A parameter can be written as name: str, a return value as -> int, and a variable as count: int.

Collections can describe their contents, such as list[str] or dict[str, int]. A union such as str | int means either type is expected. str | None means a string or None is expected. A protocol describes required behavior rather than requiring one specific class. Generics let one annotation work with several related types while keeping useful type information.

Editors, linters, and static type checkers can inspect these annotations. Normal Python execution usually does not reject a value just because it conflicts with a hint. Python remains dynamically typed.

Use type hints for clearer interfaces, safer refactoring, better editor help, and static checking. Do not treat them as input validation. Values from users, files, networks, or external services should be validated at runtime when correctness or safety depends on the actual value.

Where it is used

Type hints are common in production Python libraries, service code, application interfaces, shared utility functions, and large code bases where developers need to understand expected values. They are useful for editor assistance, static checking, safer refactoring, clearer public interfaces, and describing collections or reusable components. Runtime validation is still needed when values come from untrusted or external sources and the program must confirm their real types or structure.

Why Interviewers Ask This

Interviewers ask this to check whether a candidate understands what Python type hints describe, how they help people and development tools, and what Python actually does with them during normal execution. They also want to see whether the candidate can separate static checking from runtime validation and choose useful annotations without assuming that hints automatically enforce types.

Common interview mistakes

A common mistake is saying that Python automatically rejects values that do not match type hints. Normal execution usually does not do that. Another mistake is confusing static type checking with runtime validation. Static checking examines annotations without relying on normal program execution, while runtime validation examines actual values while the program runs. Candidates also sometimes think str | None means a required string. It means either a string or None is expected. Another mistake is adding complicated annotations that make simple code harder to understand without providing useful checking value.

Interview tip

Start by saying that type hints describe expected types but normally do not enforce them at runtime. Then give one parameter and return example, mention collections and unions, and finish by clearly separating static checking from runtime validation.

Interviewer may ask next
What happens if I pass a value that does not match a Python type hint?

Normal Python execution usually still accepts the call because most type hints are not automatically enforced at runtime. For example, a function annotated with a str parameter can still receive another type unless the function or another runtime tool checks it. A static type checker may report the mismatch. This matters because annotations improve guidance and static checking, but they are not a replacement for runtime validation when actual input must be verified.

When should a production Python application use runtime validation in addition to type hints?

Use runtime validation when the real value must be checked while the program is running, especially for input from users, files, APIs, networks, or other external systems. Type hints still help developers and static checking tools understand the expected type or structure. Runtime validation adds actual enforcement. The tradeoff is extra code and runtime work, so controlled internal values may only need type hints, while untrusted boundaries often need both.

64. What is a Python dataclass?Language SpecificEasy

Question Details

Define a dataclass as a normal Python class decorated with @dataclass so common special methods can be generated from annotated fields. Explain generated __init__, repr, equality, defaults and default factories, frozen and slots options, post-initialization, inheritance, and the difference between a dataclass, a plain dictionary, and a validation or serialization library.

Short Interview Answer (30-60 seconds)

A Python dataclass is a normal class decorated with @dataclass. Python can generate common methods such as init, repr, and eq from the annotated fields. I use it when a class mainly stores related data and I still want clear named fields and normal class behavior. For mutable defaults such as lists, I use default_factory so each object gets its own value.

Detailed Explanation

A dataclass is useful when you want to group related information into one clear object without writing the same setup code again and again. You describe the pieces of information that the object should hold, and Python can create much of the routine setup for you. This makes the class shorter and easier to read. It works well for records such as a user, order, configuration, or message. It is still a normal class, so you can add your own methods and rules when needed.

Useful Questions to Ask the Interviewer
  1. Would you like me to cover options such as frozen and slots?
  2. Should I compare dataclasses with dictionaries and validation libraries?
What is a Python dataclass? diagram
How to Explain It in an Interview

A dataclass is a normal Python class decorated with @dataclass. Python reads its annotated fields and can generate methods such as init, repr, and eq. By default, equality compares the class and the values of fields marked for comparison.

Fields can have default values. For mutable values such as lists, use field with default_factory so each instance receives a new object. This avoids sharing one mutable value between instances.

The frozen option blocks normal assignment to dataclass fields after creation. It does not make contained mutable objects deeply immutable, and it is not a security boundary. The slots option asks the dataclass to create a class with slots. This can reduce per instance memory use because instances normally do not need their own dict, although inheritance and base class behavior can affect the final layout.

__post_init__ runs after the generated init finishes and is useful for derived values or extra checks. Dataclasses support inheritance, but required fields cannot follow fields with defaults across the final inherited field order.

Use a dataclass for structured application data with normal class behavior. A dictionary is more flexible but gives less declared structure. A dataclass also does not automatically perform runtime type validation or provide a complete external data validation and serialization system.

Where it is used

Dataclasses are useful for configuration objects, domain records, messages passed between parts of an application, parsed internal data, test fixtures, and small result objects. They work well when the fields are known in advance and the object may also need methods. In production, default_factory is important for mutable fields. frozen can express that normal field reassignment should not happen after creation. slots can reduce memory use when many small instances are created, but the actual benefit depends on the class hierarchy and workload.

Why Interviewers Ask This

Interviewers ask this question to check whether a candidate understands how Python can remove repetitive class code while keeping normal class behavior. They also want to see whether the candidate understands generated methods, field defaults, mutable defaults, equality, frozen objects, slots, inheritance, and initialization hooks. A strong answer also shows good judgment about when a dataclass is enough and when a dictionary or a separate validation or serialization library is a better choice.

Common interview mistakes

A common mistake is thinking a dataclass is a special container that is separate from normal Python classes. It is still a normal class. Another mistake is expecting field annotations to validate values at runtime automatically. They do not. Developers may also expect a dataclass to provide complete JSON serialization automatically, which it does not. Mutable defaults such as lists should use default_factory so each instance gets its own object. frozen does not make contained mutable objects deeply immutable. With inheritance, required and default field ordering must remain valid across the combined inherited fields.

Interview tip

Start by saying that a dataclass is a normal class where @dataclass can generate common methods from annotated fields. Then mention init, repr, eq, defaults, and default_factory. Finish with frozen, slots, post_init, inheritance, and the key distinction that a dataclass gives structure and convenience but does not automatically provide runtime validation or a complete serialization system.

Interviewer may ask next
What happens if a dataclass field needs a list as its default value?

Use field with default_factory so Python calls a factory for each new instance and gives that instance its own list. This matters because mutable state should not be unintentionally shared between instances. The main tradeoff is a small amount of extra setup syntax in exchange for correct and predictable object state.

When would you choose a dataclass instead of a dictionary or a validation library?

Choose a dataclass when the data has a known structure and you want named fields, generated class methods, and normal class behavior. Choose a dictionary when the shape is loose or highly dynamic. Choose a validation or serialization library when external data needs runtime validation, conversion, schemas, or richer serialization support. The tradeoff is that a dataclass is simple and part of the Python standard library, but it does not provide those larger validation and serialization features by itself.

65. What is structural pattern matching in Python?Language SpecificMedium

Question Details

Explain match and case semantics, literal, sequence, mapping, class, OR, capture, and wildcard patterns, guards, and the difference between pattern matching and a simple switch statement.

Short Interview Answer (30-60 seconds)

Structural pattern matching lets Python inspect both the value and the structure of an object with match and case. Python evaluates the subject once, checks cases from top to bottom, and runs the first case whose pattern matches and whose guard is true. Patterns can test literals, sequences, mappings, classes, alternatives, and nested data while capturing useful parts. This makes it more powerful than a simple switch statement.

Detailed Explanation

See the Code while reading this explanation.

Use structural pattern matching when a program must handle several clear shapes of data. It is available from Python 3.10.

Useful Questions to Ask the Interviewer
  1. Should I focus on Python language behavior, or also explain the runtime and standard library?
  2. Which Python version and execution environment should I assume?
  3. Would you like a small code example together with production tradeoffs and edge cases?

A match statement evaluates its subject once. Python checks each case in order. It runs the first case whose pattern succeeds and whose optional guard is true.

A literal pattern checks a fixed value. A sequence pattern checks and extracts items from supported sequence objects, but it does not treat strings, bytes, byte arrays, or iterators as sequence patterns. A mapping pattern checks required keys and ignores extra keys unless the pattern captures them. A class pattern uses an instance check and can inspect attributes. An OR pattern accepts any listed alternative. A capture pattern binds a value to a name. The underscore wildcard accepts any value without binding it. A guard adds an extra if condition after the pattern succeeds.

This is more than a simple switch. It can inspect nested structure and extract data instead of only comparing one value with fixed choices.

Use it for structured commands, events, parsed data, and domain objects. Avoid it when a small if and elif chain is clearer. Put specific patterns before broad capture or wildcard patterns.

What is structural pattern matching in Python? diagram
Example

The example matches one value named event. It demonstrates literal, OR, sequence, mapping, class, capture, wildcard, and guard patterns. Python evaluates event once and checks the cases from top to bottom. The first case whose pattern succeeds and whose guard is true returns a result. The specific payment cases appear before the general mapping case so they are not hidden by a broader pattern. The final wildcard handles every value that earlier cases do not handle.

Code
from dataclasses import dataclass
from typing import Any


@dataclass
class UserEvent:
    # A dataclass supports class patterns for its fields.
    name: str
    active: bool


def describe_event(event: Any) -> str:
    # Python evaluates event once and checks each case in order.
    match event:
        # Literal pattern: match the exact singleton value None.
        case None:
            return "No event"

        # OR pattern: accept either literal command.
        case "start" | "begin":
            return "Start command"

        # Sequence pattern: require exactly three items.
        # The values in the second and third positions are captured.
        case ["move", x, y]:
            return f"Move to {x}, {y}"

        # Mapping pattern with a guard.
        # Extra mapping keys are allowed and ignored here.
        case {"type": "payment", "amount": amount} if amount > 0:
            return f"Valid payment of {amount}"

        # The pattern still matches when the amount is not positive.
        # This case runs after the guard above is false.
        case {"type": "payment", "amount": amount}:
            return f"Invalid payment amount: {amount}"

        # Class pattern: check the object type and inspect attributes.
        case UserEvent(name=name, active=True):
            return f"Active user: {name}"

        # Capture pattern: store the value of the type key.
        case {"type": event_type}:
            return f"Other event type: {event_type}"

        # Wildcard pattern: accept anything not handled above.
        # The underscore does not bind a new variable.
        case _:
            return "Unknown event"


if __name__ == "__main__":
    examples = [
        None,
        "start",
        ["move", 10, 20],
        {"type": "payment", "amount": 50, "currency": "USD"},
        {"type": "payment", "amount": 0},
        UserEvent(name="Asha", active=True),
        {"type": "logout"},
        42,
    ]

    # Run every example so this file can be copied and executed.
    for example in examples:
        print(describe_event(example))
Where it is used

Structural pattern matching is useful for processing structured API responses, application commands, event messages, parsed syntax trees, configuration records, and domain objects with several known forms. It works well when each data shape has a clear action and useful values must be extracted. Its runtime cost depends on the patterns used, including length checks, equality checks, mapping lookups, instance checks, and attribute access. Matching normally binds references instead of copying the whole subject. A starred sequence capture creates a new list, and a double star mapping capture creates a new dictionary for the remaining items.

Why Interviewers Ask This

Interviewers ask this question to check whether a candidate understands modern Python syntax, case selection, data extraction, guards, and pattern order. It also tests whether the candidate can choose pattern matching when it improves clarity instead of treating it as a direct replacement for every if statement.

Common interview mistakes

A common mistake is placing a broad capture or wildcard pattern before specific cases. An unguarded capture or wildcard pattern always succeeds, so Python requires an irrefutable case to be last. Another mistake is treating a plain name in a pattern as a constant. A plain name is normally a capture pattern, while a named constant must normally use a qualified name such as Color.RED. Developers may also expect sequence patterns to match strings or iterators, but they do not. Other mistakes include assuming mapping patterns reject extra keys, forgetting that guards run only after a pattern succeeds, and relying on variable bindings produced during a failed pattern because that behavior is not guaranteed.

Interview tip

Start with the runtime rule: Python evaluates the subject once, checks cases in order, and selects the first pattern with a true guard. Then explain that patterns can inspect structure and capture values. Give one mapping or sequence example. Finish by saying that match is more powerful than a simple switch but should be used only when it makes structured decisions clearer.

Interviewer may ask next
What happens when a pattern matches but its guard is false?

Python skips that case body and continues checking later cases because a case is selected only when both its pattern succeeds and its guard is true. Values needed by the guard are captured before the guard runs. Guard expressions can raise exceptions or cause side effects, so production code should keep them simple and predictable.

What performance and memory costs can structural pattern matching add?

The cost depends on the exact pattern because Python may perform equality checks, length checks, mapping lookups, instance checks, and attribute access. There is no single complexity for every match statement. Ordinary captures bind references and do not copy the complete subject. However, a starred sequence capture builds a new list, and a double star mapping capture builds a new dictionary, so those forms use additional memory.

66. How do type hints work at runtime?Language SpecificMedium

Question Details

Explain that annotations normally do not enforce types by themselves, how __annotations__ stores metadata, how static type checkers use it, and how generics, unions, and forward references are represented.

Short Interview Answer (30-60 seconds)

Type hints normally do not enforce types at runtime. Python keeps annotations as metadata, usually available through __annotations__, while static type checkers use them before execution. Runtime tools can inspect and act on the metadata, but they must perform their own validation. Generic types, unions, and forward references are represented as annotation values whose exact runtime form can depend on the Python version and annotation settings.

Detailed Explanation

See the Code while reading this explanation.

Type hints normally do not reject a value at runtime. Python still follows dynamic typing, so a function can receive a value that does not match its annotation unless some other code checks it.

Useful Questions to Ask the Interviewer
  1. Should I focus on Python language behavior, or also explain the runtime and standard library?
  2. Which Python version and execution environment should I assume?
  3. Would you like a small code example together with production tradeoffs and edge cases?

Annotations are metadata associated with functions, classes, and modules. They are commonly available through __annotations__. In Python 3.14, annotations are evaluated lazily by default, so reading __annotations__ may evaluate annotation expressions and can raise an exception. In earlier versions, annotations were usually evaluated when the definition ran unless postponed annotation behavior was enabled.

Static type checkers read type hints without changing normal program execution. Editors also use them for completion and warnings. Runtime tools can use typing.get_type_hints to resolve annotations, including many forward references. Resolution may execute annotation code, allocate a result dictionary, and fail when a required name is missing.

A generic such as list[str] is represented as a generic alias. A union such as int | str is represented as a union type. A forward reference may remain a string or another delayed form until resolved.

Type hints improve clarity and tooling, but they do not replace validation of API requests, files, database values, or other external input.

How do type hints work at runtime? diagram
Example

The example defines a User class and a function with three annotation forms. list[User] is a generic alias. int | None is a union. The return annotation is str. The first call shows normal use. The second call proves that Python does not automatically enforce the limit annotation. The function completes because its own runtime logic accepts the supplied string value. The example then reads __annotations__ and uses get_type_hints to obtain resolved type information. The exact printed representation can vary between supported Python versions, but the runtime behavior and conclusion remain the same.

Code
from typing import get_type_hints


class User:
    """A small class used in the annotation example."""

    def __init__(self, name: str) -> None:
        # This annotation describes the expected value.
        # Python does not enforce it by itself.
        self.name = name


def describe_users(users: list[User], limit: int | None = None) -> str:
    """Return names from the supplied users."""

    # This comparison works with both integers and the string used below.
    # It lets the example prove that Python accepted the wrong runtime type.
    if limit is None:
        selected_users = users
    elif limit == 1:
        selected_users = users[:1]
    else:
        selected_users = users

    return ", ".join(user.name for user in selected_users)


users = [User("Asha"), User("Luis")]

# This call matches the annotations.
print(describe_users(users, 1))

# This call does not match the limit annotation.
# Python still allows it because type hints are not automatic checks.
print(describe_users(users, "one"))

# Access the annotation metadata associated with the function.
print(describe_users.__annotations__)

# Resolve the annotations into runtime type information.
print(get_type_hints(describe_users))
Where it is used

Type hints are used in application services, APIs, libraries, data models, tests, and shared interfaces. Static type checkers use them to find likely mistakes before deployment. Editors use them for suggestions and warnings. Frameworks and validation libraries may inspect annotations at runtime to build schemas, connect dependencies, serialize data, or validate input. External values still require runtime validation. Annotation inspection should not be repeated on every request when the resolved result can be safely reused, because resolution performs work and creates result objects.

Why Interviewers Ask This

Interviewers ask this to check whether the candidate understands the difference between type information and runtime enforcement. It also tests knowledge of annotation storage, static analysis, runtime inspection, forward reference resolution, and safe production use.

Common interview mistakes

A common mistake is assuming that Python automatically rejects arguments that do not match annotations. Another mistake is treating __annotations__ as validated data rather than metadata. Developers may also assume annotations always have the same runtime representation in every Python version. Lazy evaluation, postponed annotation behavior, and forward references can change what is stored or when it is evaluated. Another mistake is calling get_type_hints on untrusted annotations without considering that annotation evaluation can execute code. Repeatedly resolving the same annotations in a busy request path can also add avoidable processing and allocation.

Interview tip

Begin with the conclusion that type hints do not normally enforce types. Then explain static checking, __annotations__, and runtime inspection with get_type_hints. Mention that Python 3.14 evaluates annotations lazily by default. Finish by explaining how a generic, a union, and a forward reference appear at runtime.

Interviewer may ask next
What happens if a forward reference cannot be resolved?

The resolution can fail when the referenced name is not available in the required namespace. The annotation may still exist in a delayed or string form, but get_type_hints using value resolution can raise NameError or another evaluation exception. This matters when types are imported only for static checking or are defined in a different scope. Production code should provide the correct namespaces or avoid resolving the annotation until the required names are available.

What are the production costs and risks of inspecting type hints at runtime?

Runtime inspection adds processing and memory work because Python may evaluate annotation expressions, resolve names, and create a dictionary of results. get_type_hints may also execute code contained in annotations, so it should not be used carelessly with untrusted definitions. The main tradeoff is that runtime inspection enables schema creation, dependency handling, and validation tools, but repeated resolution in a busy path can waste resources. A production system can resolve trusted annotations once and safely reuse the result when appropriate.

67. How do dataclasses generate class behavior?Language SpecificMedium

Question Details

Explain how @dataclass derives methods such as __init__, __repr__, and __eq__, how field defaults and default_factory work, and how frozen, order, slots, and post-initialization options change behavior.

Short Interview Answer (30-60 seconds)

A dataclass inspects the annotated fields in a class and can generate methods such as __init__, __repr__, and __eq__. This removes repeated class code while keeping the fields clear. I use default_factory for mutable defaults, __post_init__ for validation or derived values, frozen to block normal field assignment, order for field based comparisons, and slots when many small objects need lower memory use.

Detailed Explanation

See the Code while reading this explanation.

The practical benefit of @dataclass is that it generates common class behavior from annotated fields. By default, it creates __init__, __repr__, and __eq__. The generated __init__ accepts field values and stores them on the object. The generated __repr__ shows the class name and fields. The generated __eq__ compares objects of the same class using fields marked for comparison.

Useful Questions to Ask the Interviewer
  1. Should I focus on Python language behavior, or also explain the runtime and standard library?
  2. Which Python version and execution environment should I assume?
  3. Would you like a small code example together with production tradeoffs and edge cases?

A normal default is reused as the declared default value. Mutable defaults such as lists must use default_factory. The factory runs for each new object, so objects do not accidentally share one list.

The frozen option blocks normal assignment and deletion of fields after initialization. It does not make mutable values inside the object immutable. The order option generates comparison methods from fields in declaration order and requires equality support. The slots option creates slotted instances without the usual instance dictionary, which can reduce memory use and prevents undeclared attributes.

After the generated __init__ finishes, __post_init__ runs. It is useful for validation and derived fields. In a frozen dataclass, object.__setattr__ is needed to set a derived field during this step. Dataclasses work best for clear data models. A regular class is often better when construction rules or behavior are highly complex.

How do dataclasses generate class behavior? diagram
Example

The example defines an ordered, frozen, and slotted Product dataclass. The name and price fields become parameters in the generated __init__. The tags field uses default_factory, so each Product receives a separate list. It is excluded from equality, ordering, and hashing because compare is false and hash follows that setting by default. The display_name field has init set to false, so callers cannot pass it to the constructor. After initialization, __post_init__ validates the price and calculates display_name. Because the object is frozen, object.__setattr__ is used during post initialization. The generated __repr__ displays the fields, __eq__ compares the selected fields, and the generated ordering methods compare name first and price second.

Code
from dataclasses import FrozenInstanceError, dataclass, field


@dataclass(frozen=True, order=True, slots=True)
class Product:
    # These fields become parameters in the generated __init__ method.
    name: str
    price: float

    # default_factory creates a new list for every Product object.
    # compare=False excludes this field from equality and ordering.
    tags: list[str] = field(default_factory=list, compare=False)

    # This field is calculated after the generated initialization finishes.
    # Callers cannot pass it to __init__ because init is false.
    display_name: str = field(init=False, compare=False)

    def __post_init__(self) -> None:
        # Validate the values received by the generated __init__ method.
        if self.price < 0:
            raise ValueError("price cannot be negative")

        # Normal assignment is blocked because the dataclass is frozen.
        # object.__setattr__ allows this derived value to be set here.
        object.__setattr__(
            self,
            "display_name",
            f"{self.name}: ${self.price:.2f}",
        )


first = Product("Keyboard", 49.99, ["hardware"])
second = Product("Mouse", 29.99)
third = Product("Keyboard", 49.99, ["sale"])

# The generated __repr__ displays the dataclass fields.
print(first)

# The generated __eq__ compares name and price.
# tags and display_name are ignored because compare is false.
print(first == third)

# The generated ordering methods compare name first and price second.
print(sorted([first, second]))

# Each object receives a different list from default_factory.
print(first.tags is second.tags)

# __post_init__ created this derived value.
print(first.display_name)

# Frozen objects reject normal field assignment.
try:
    first.price = 10.0
except FrozenInstanceError as error:
    print(type(error).__name__)
Where it is used

Dataclasses are used for configuration values, API request data, service results, domain records, test fixtures, parsed records, and internal messages. Frozen dataclasses are useful when field references should not change after creation. Ordered dataclasses are useful when declaration order matches the required sorting rule. Slotted dataclasses are useful when an application creates many small objects and memory use matters.

Why Interviewers Ask This

Interviewers ask this question to check whether a candidate understands how Python creates class methods from declared fields. It also tests judgment about safe defaults, equality, ordering, controlled immutability, memory use, validation, and maintainable data models.

Common interview mistakes

A common mistake is using a list or dictionary as a direct default instead of using default_factory. Another mistake is assuming frozen makes every nested value immutable. A frozen dataclass can still contain a list whose contents can change. Developers may enable order without checking whether field declaration order matches the required comparison rule. Other mistakes include placing a required field after a field with a default, expecting __post_init__ to replace all complex construction logic, forgetting that equality requires the same class, and assuming slots supports undeclared instance attributes.

Interview tip

Start by saying that @dataclass generates standard methods from annotated fields. Then explain default_factory for safe mutable defaults. Finish by stating how frozen, order, slots, and __post_init__ change the generated behavior.

Interviewer may ask next
Does frozen make every value inside a dataclass immutable?

No. Frozen blocks normal assignment and deletion of dataclass fields, but it does not make nested mutable values immutable. A frozen dataclass can contain a list whose contents can still change. This matters because callers may incorrectly assume the whole object cannot be modified. Use immutable values such as tuples when nested values must also remain unchanged.

When should you use slots in a dataclass?

Use slots when the allowed attributes are known and the program creates many small objects. Slots removes the usual instance dictionary, which can reduce memory use and prevent accidental undeclared attributes. Attribute access may also be slightly faster, but that should not be assumed without measurement. The main tradeoff is reduced flexibility, and inheritance, weak references, inspection, or serialization tools may need additional care.

68. What is the difference between a shallow copy and a deep copy in Python?Language SpecificMedium

Question Details

Explain which objects are copied at each level, how nested mutable objects behave, how copy.copy and copy.deepcopy differ, and when each approach is appropriate.

Short Interview Answer (30-60 seconds)

A shallow copy creates a new outer object, but it keeps references to the same nested objects. A deep copy creates a new outer object and recursively copies nested objects when needed. I use a shallow copy when sharing nested values is safe. I use a deep copy when nested mutable data must be independent.

Detailed Explanation

See the Code while reading this explanation.

The practical difference is what happens to nested objects. A shallow copy creates a new outer container, but its items still refer to the same objects used by the original. If a list contains another list, changing that inner list through the shallow copy is visible through the original.

Useful Questions to Ask the Interviewer
  1. Should I focus on Python language behavior, or also explain the runtime and standard library?
  2. Which Python version and execution environment should I assume?
  3. Would you like a small code example together with production tradeoffs and edge cases?

The copy.copy function performs a shallow copy. It is useful when only the outer container must be separate or when nested objects are intentionally shared.

The copy.deepcopy function recursively processes the contained objects. It creates independent copies of nested mutable objects when their copying behavior allows it. Immutable objects may still be shared because they cannot be changed. Deepcopy also keeps a record of objects it has already processed. This helps it preserve shared relationships and handle recursive structures.

A deep copy can require more processing time and memory because Python may visit much of the reachable object graph. Classes can customize copying through special methods, and some resource based objects, such as open files, should not be treated as ordinary data copies.

In production, I choose the smallest level of copying that gives the required isolation. This avoids accidental shared changes without copying more data than necessary.

What is the difference between a shallow copy and a deep copy in Python? diagram
Example

The example creates a nested list and then makes a shallow copy and a deep copy. The shallow copy has a new outer list, but it shares both inner lists with the original. Appending 5 through the shallow copy therefore changes the inner list seen by the original. The deep copy has separate inner lists. Appending 6 through the deep copy therefore does not change the original.

Code
import copy

# Create an outer list that contains two mutable inner lists.
original = [[1, 2], [3, 4]]

# Create a new outer list.
# Its inner lists are still shared with the original.
shallow = copy.copy(original)

# Create a new outer list and separate copies of the inner lists.
deep = copy.deepcopy(original)

# Change the first shared inner list through the shallow copy.
shallow[0].append(5)

# Both values contain 5 because they refer to the same first inner list.
print("Original after shallow change:", original)
print("Shallow copy:", shallow)

# Change the second inner list through the deep copy.
deep[1].append(6)

# Only the deep copy contains 6 because its second inner list is separate.
print("Original after deep change:", original)
print("Deep copy:", deep)
Where it is used

A shallow copy is useful when creating a separate outer configuration dictionary while nested values are immutable or intentionally shared. A deep copy is useful in tests, simulations, document editing, and data transformation when nested mutable data must be changed without affecting the original. In production, deep copying should be used carefully with large object graphs because it may increase processing time and memory use.

Why Interviewers Ask This

Interviewers ask this question to check whether a candidate understands Python object references, nested mutable objects, and the behavior of the copy module. It also tests whether the candidate can choose the required level of isolation while considering processing time and memory use.

Common interview mistakes

A common mistake is assuming that a shallow copy makes every nested object independent. Another mistake is using list.copy or slicing on a nested list and expecting deep copy behavior. Developers may also use deepcopy for every object without considering its processing and memory cost. It is also incorrect to assume that deepcopy always creates a new instance for every value. Immutable objects may be shared, and classes can define custom copy behavior.

Interview tip

Start with the practical rule. A shallow copy separates only the outer object, while a deep copy also separates nested mutable objects when needed. Then show one nested list example and mention the processing and memory tradeoff.

Interviewer may ask next
What happens when the copied container contains only immutable objects?

A shallow copy is normally enough because immutable objects cannot be changed in place. The outer container is new, but values such as integers and strings may remain shared safely. This matters because a deep copy would usually perform extra work without providing useful isolation.

Why should deepcopy be used carefully with a large object graph?

Deepcopy may visit and process most objects reachable from the original value, so it can require significant processing time and memory. The exact cost depends on the size and structure of the graph and on custom copy behavior. The main tradeoff is stronger isolation against greater resource use, so production code should copy only as deeply as the required behavior demands.

69. What are async and await in Python?Language SpecificEasy

Question Details

Define async def as creating a coroutine function and await as suspending that coroutine until an awaitable can make progress. Explain the event loop, cooperative scheduling, tasks, asynchronous I/O, sequential versus concurrent awaits, cancellation, blocking calls, and why asyncio helps I/O-bound concurrency but does not automatically make CPU-bound Python code faster.

Short Interview Answer (30-60 seconds)

async def creates a coroutine function, and await lets its coroutine pause while an awaitable is not ready to complete. Control can then return to the event loop so other ready tasks can run. This is useful for input and output work such as network requests. Awaiting independent operations one after another is still sequential, while scheduling them as tasks can let their waiting periods overlap. asyncio does not automatically make CPU intensive Python code faster.

Detailed Explanation

See the Code while reading this explanation.

The practical idea is to avoid wasting time while a program waits. Imagine a program starts a network request and must wait for a reply. Instead of doing nothing during that wait, Python can pause that piece of work and let other ready work continue. When the requested operation becomes ready, Python can continue the paused work. This is useful when a program spends much of its time waiting for networks, databases, files, or other services. It does not mean that every type of Python work becomes faster.

Useful Questions to Ask the Interviewer
  1. Would you like a small asyncio example?
  2. Should I also explain sequential and concurrent waits?
What are async and await in Python? diagram
How to Explain It in an Interview

In Python, async def creates a coroutine function. Calling it creates a coroutine object. Its body starts running only when the coroutine is awaited or scheduled.

Inside a coroutine, await works with an awaitable object. If that operation is not ready, the coroutine can suspend and give control back to the event loop. The event loop then runs other ready tasks. This is cooperative scheduling because running code must reach a point that allows other work to run.

A Task schedules a coroutine with the event loop. Awaiting two independent operations one after another keeps them sequential. Creating both tasks first can let their waiting periods overlap.

Cancellation is normal in asyncio. A cancelled task usually receives asyncio.CancelledError when cancellation is delivered, so cleanup should use constructs such as try and finally when needed.

Blocking calls and CPU intensive Python work can stop the event loop from serving other tasks. asyncio is mainly useful for input and output concurrency. It does not automatically speed up CPU intensive Python code.

Example

The example creates two coroutine tasks before waiting for their results. Each coroutine reaches asyncio.sleep, which suspends that coroutine without blocking the event loop. While one task is waiting, the event loop can run the other ready task. asyncio.gather waits for both scheduled tasks and returns their results. This demonstrates concurrent waiting for independent input and output style operations without claiming that Python executes the coroutine bodies in parallel.

Code
import asyncio


async def fetch(name: str, delay: float) -> str:
    # Define an asynchronous operation that represents waiting for input or output.
    print(f"Starting {name}")

    # Pause this coroutine without blocking the event loop.
    await asyncio.sleep(delay)

    print(f"Finished {name}")
    return name


async def main() -> None:
    # Schedule both coroutines before waiting for their results.
    # Their waiting periods can overlap because the event loop can run
    # another ready task while one task is suspended at await.
    first_task = asyncio.create_task(fetch("first", 1.0))
    second_task = asyncio.create_task(fetch("second", 1.0))

    # Wait for both tasks and collect their results.
    first_result, second_result = await asyncio.gather(
        first_task,
        second_task,
    )

    print(first_result, second_result)


# Create an event loop, run main until it finishes, and close the loop.
asyncio.run(main())
Where it is used

asyncio is useful in production services that handle many independent operations that spend time waiting. Examples include calling several web services, handling many network connections, waiting for database operations through an asynchronous driver, and coordinating background input and output work. It is most useful when many waits can overlap without blocking the event loop.

Why Interviewers Ask This

Interviewers ask this to check whether a candidate understands how Python handles waiting work without blocking other useful work. They want to see knowledge of coroutine functions, awaitable objects, the event loop, tasks, cooperative scheduling, cancellation, blocking calls, and the difference between input and output concurrency and CPU intensive work.

Common interview mistakes

A common mistake is thinking that async makes every function run in parallel. It does not. Another mistake is awaiting independent operations one by one and expecting concurrency. That keeps their waits sequential. Developers may also call blocking functions inside a coroutine. A blocking call can stop the event loop and delay other tasks. Another mistake is ignoring cancellation and cleanup. Finally, asyncio should not be treated as an automatic speed improvement for CPU intensive Python work.

Interview tip

Start with the practical idea that async and await let Python use waiting time for other work. Then explain that async def creates a coroutine function, await can suspend its coroutine, and the event loop runs other ready tasks. Clearly separate sequential awaits from concurrently scheduled tasks, and mention that blocking or CPU intensive work can still stop the event loop.

Interviewer may ask next
What happens if I call a blocking function inside an async function?

The blocking function can block the event loop thread. While it is running, other asyncio tasks on that loop may not get a chance to make progress. This matters because cooperative scheduling depends on running code returning control to the event loop. For blocking input and output work that has no asynchronous interface, asyncio.to_thread can move the call to a worker thread when appropriate. CPU intensive work may instead need a separate process so it does not block the event loop.

What is the difference between awaiting two coroutines sequentially and scheduling them as tasks?

Sequential awaits wait for the first operation to finish before continuing to the second await. If the operations are independent, scheduling both as tasks first lets the event loop make progress on either task while the other is waiting. This can reduce total waiting time for input and output operations. The tradeoff is added coordination, cancellation, error handling, and resource management, so concurrent tasks should be used when the operations are independent and can safely overlap.

70. How are coroutines scheduled by asyncio?Language SpecificHard

Question Details

Explain coroutine objects, tasks, the event loop, awaiting, suspension points, cooperative scheduling, cancellation, and why blocking calls can stall all tasks on the loop.

Short Interview Answer (30-60 seconds)

Asyncio uses an event loop and cooperative scheduling. Calling an async function creates a coroutine object, but does not run it. Awaiting that coroutine runs it as part of the current task. Creating a task schedules it to run independently when the event loop gets control. Each task runs until it reaches an await that must wait. It then suspends so the event loop can run other ready work. Because the loop does not interrupt normal Python code, one blocking call or long calculation can delay every task on that loop.

Detailed Explanation

Asyncio schedules coroutine work through an event loop. Calling an async function creates a coroutine object. The object holds the future execution state, but does not start by itself.

Useful Questions to Ask the Interviewer
  1. Should I focus on Python language behavior, or also explain the runtime and standard library?
  2. Which Python version and execution environment should I assume?
  3. Would you like a small code example together with production tradeoffs and edge cases?

Awaiting a coroutine runs it as part of the current task. Creating a task registers the coroutine with the event loop so it can make progress independently. The loop selects ready tasks and lets each task run until it finishes, raises an exception, or reaches an await that cannot complete yet.

At that suspension point, the task saves its state and gives control back to the loop. When the awaited operation becomes ready, the loop schedules the task to continue. An await may not suspend when its result is already available.

This is cooperative scheduling. Asyncio provides concurrency, but does not make ordinary Python code run in parallel on one event loop. Blocking input and output, time.sleep, or long calculation can stall every task because the loop cannot run other work until control returns.

Each coroutine and task also uses memory for frames, local values, state, and results. Task switching has overhead, but it is usually small compared with the waiting time saved in input and output heavy programs.

How are coroutines scheduled by asyncio? diagram
Where it is used

Asyncio is useful in programs that manage many waiting operations at the same time. Examples include web servers, API clients, database connections, web sockets, message consumers, timers, and network services. It is most useful when tasks spend much of their time waiting for input and output. Use asynchronous libraries when possible. Move unavoidable blocking input and output to a worker thread. Move heavy calculation to a separate process or another suitable execution service so the event loop remains responsive.

Why Interviewers Ask This

Interviewers ask this question to check whether a candidate understands how asynchronous Python code actually runs. They are evaluating knowledge of coroutine objects, tasks, the event loop, suspension, cancellation, blocking work, and the difference between concurrency and parallel execution.

Common interview mistakes

Common mistakes include believing that calling an async function starts it, creating coroutine objects without awaiting or scheduling them, and assuming that every await always gives control to another task. Another mistake is treating concurrency as parallel execution. Developers may also call time.sleep, use a blocking library, or perform long calculation directly on the event loop. This prevents other tasks from running. It is also incorrect to assume that cancel stops a task immediately. Cancellation is a request and cleanup may still need to run. Fire and forget tasks should be stored and their exceptions should be observed.

Interview tip

Explain the runtime flow in this order: coroutine object, current task or new task, event loop, await, suspension, and resumption. Then state the main limitation clearly. Asyncio is cooperative, so code must return control to the event loop and must not perform blocking work there.

Interviewer may ask next
Does every await allow another task to run?

No. An await suspends the current task only when the awaited operation is not ready. When the result is already available, execution may continue without giving another task a chance to run. This matters because placing await in code does not guarantee fairness or prevent a long section of work from delaying other tasks.

How should cancellation and blocking work be handled in production asyncio code?

Cancellation should be treated as a request, and blocking work should be kept off the event loop. A cancelled task normally receives CancelledError when it next runs, so cleanup should use try and finally and should usually allow cancellation to continue. Blocking input and output can run in a worker thread, while heavy calculation may need a separate process. These choices keep the loop responsive, but add scheduling, memory, and coordination cost.

More questions load as you scroll

Disclaimer: This interview guide is for educational and informational purposes only. It is designed to help readers prepare, but it does not guarantee any interview result, hiring decision, offer, or outcome. Interview questions, hiring criteria, and preferred answers can vary by employer, interviewer, industry, location, and time. The examples and explanations reflect the authors' research and judgment, are provided without warranties of any kind, and should not be treated as the only correct approach. Diagrams are simplified illustrations intended to highlight the main components and their interactions; actual systems and implementations may be more complex. Alternative approaches may be equally valid or better suited to a particular question, context, or interviewer. To the fullest extent permitted by applicable law, the author, contributors, and publisher are not liable for decisions made, actions taken, or losses incurred based on this guide.

Content Accuracy and Verification: To the fullest extent permitted by applicable law, we do not represent or warrant that interview guides, questions, answers, examples, or diagrams are accurate, complete, current, error-free, or suitable for any particular purpose. You are responsible for independently reviewing and verifying the information before relying on it.