Table of Contents

If you're confident with basic constructs and have experience writing small projects or working with popular libraries, it's time to move to the intermediate level.

Interviews at this stage are more in-depth: they assess not just syntax knowledge but also your understanding of language principles, logic, and the ability to work with more complex data structures. Interviewers expect you to be comfortable with list comprehensions, understand generators, write functions with variable arguments, handle exceptions, and apply OOP fundamentals.

The intermediate level is where a programmer's systematic thinking is formed. The questions in this section will help you assess how deeply you understand the language's features, whether you can optimize solutions, and if you can write cleaner, shorter, and more reliable code. This knowledge is especially important for those aiming to advance to mid or senior levels.

Python Interview Questions for Mid-Level Developers

At the mid-level, you're expected not just to know the language but to apply it effectively in real-world tasks. Such interviews focus on practical experience, architectural understanding, proficient use of the standard library, and solving non-trivial problems. Therefore, it's essential to prepare for questions that go beyond textbooks and cover real-world scenarios: generators, decorators, file handling, design errors, and code robustness. This section compiles such questions to help you evaluate the depth of your knowledge and identify gaps that may hinder your professional growth.

1. What is the difference between is and ==?

The == operator checks value equality of objects. It invokes the special method __eq__() to determine if the contents of two objects are identical. Even if they are different objects in memory, if their data matches, == returns True.

The is operator checks object identity, i.e., whether two variables point to the same memory location. This comparison occurs at the id() level without analyzing the content.

In practice, is is used in limited cases—such as checking for None, True, False, or singletons. In all other scenarios where content comparison is needed, == is used.

Choosing between is and == incorrectly can lead to unexpected results, especially when comparing strings, lists, numbers, and other objects.

2. What is the difference between a list and a tuple?

Both structures are sequences, but lists are mutable, whereas tuples are immutable. This fundamental difference determines their usage.

Lists (list) are designed to store dynamic collections of data: during program execution, elements can be added, removed, or modified. Tuples (tuple) have fixed content—once created, they cannot be changed.

Tuples are used when data immutability is crucial (e.g., when passing configurations) or for memory and speed optimization (as tuples are processed faster).

Additionally, tuples can be used as dictionary keys, whereas lists cannot (they are unhashable). This fundamental difference arises because only immutable objects can serve as keys in hash tables.

3. What does the in operator do?

The in operator checks for the presence of an element within an iterable object. Its behavior depends on the structure it's applied to:

  • For strings, it checks for a substring.
  • For lists, tuples, and sets, it checks for a specific element.
  • For dictionaries, it checks for the key, not the value.

Under the hood, it uses the __contains__() method. If this method is absent, Python iterates using __iter__() and compares values using __eq__.

The in operator is one of the most frequently used tools in the language, and understanding it correctly is critical, especially when working with conditional checks, data filtering, implementing custom collections, and iterators.

4. What are generators, and why are they needed?

Generators are a way to create iterable objects that yield values one at a time upon request, rather than loading the entire collection into memory at once. They are implemented using functions that contain the yield keyword.

Unlike regular functions, a generator maintains its state between calls and resumes execution from where it left off. This makes it ideal for processing data streams, large files, infinite sequences, and lazy evaluations.

Advantages of generators:

  • Minimal memory consumption.
  • High performance.
  • Simplicity of implementation.
  • Convenience for streaming data processing.

Generators support the iteration protocol, so they can be used in for loops, list comprehensions, any() and all() expressions, and other contexts requiring an iterable object.

5. What does the nonlocal keyword do?

The nonlocal keyword is used in closures when it's necessary to modify a variable from an outer (but not global) scope. It allows a nested function to affect variables defined in its enclosing function.

By default, assigning a value to a variable inside a function makes it local, even if a variable with the same name exists in an outer scope. Using nonlocal provides access to the existing variable and prevents the creation of a new one.

This mechanism is important when working with nested functions, creating decorators, and implementing objects without classes. Misunderstanding how nonlocal works can lead to elusive bugs, especially when managing state within nested functions.

6. What happens when using *args and **kwargs?

*args and **kwargs are mechanisms for creating functions with a variable number of arguments.

  • *args collects all positional arguments into a tuple.
  • **kwargs collects all keyword arguments into a dictionary.

This makes the function flexible and reusable, especially when the exact set of input parameters cannot be predetermined.

These constructs are also used when passing parameters to other functions or when creating decorators that work with functions of arbitrary signatures. It's important that *args precedes **kwargs; otherwise, a syntax error will occur.

Proper use of *args and **kwargs provides control over the function interface without sacrificing readability and extensibility.

7. What is None and how is it used?

None is a special singleton object in Python that represents the absence of a value. It is of the type NoneType and is commonly used to signify "no value" or "null".

Common use cases include:

  • As the default return value of functions that do not explicitly return anything.
  • As a default value for function arguments.
  • As a placeholder to indicate that a variable has not been assigned a value yet.
  • In conditional statements to check for the absence of a value.

Comparisons with None should be made using the is operator, not ==, to ensure identity comparison.

It's important to distinguish None from other values like 0, False, [], or "". While these values evaluate to False in a boolean context, they are not equivalent to None.

8. How does try/except/finally work?

The try block is used to wrap code that might raise exceptions. If an exception occurs, it is caught by the corresponding except block, where it can be handled appropriately. An optional else block can be used to execute code if no exceptions were raised in the try block. The finally block, if present, will execute regardless of whether an exception was raised or not, making it ideal for cleanup actions like closing files or releasing resources.

Best practices include:

  • Handling only specific exceptions that you expect and can manage.
  • Avoiding bare except clauses that catch all exceptions indiscriminately.
  • Ensuring that exceptions are not silently ignored without logging or appropriate handling.

9. What is the difference between shallow copy and deep copy?

A shallow copy creates a new object, but does not create copies of nested objects; instead, it copies references to them. This means that changes to nested objects in the copy will reflect in the original. A deep copy, on the other hand, creates a new object and recursively copies all nested objects, resulting in a completely independent copy.

Use shallow copies when you need a new container object but can share the nested objects. Use deep copies when you need a completely independent copy of the entire object hierarchy.

10. What is the difference between instance methods, class methods, and static methods?

Instance methods are the most common type of methods in Python classes. They take self as the first parameter and can access and modify the instance's attributes and other methods.

Class methods take cls as the first parameter and can access and modify class state that applies across all instances of the class. They are defined using the @classmethod decorator.

Static methods do not take self or cls as the first parameter and cannot access or modify the class or instance state. They are defined using the @staticmethod decorator and are used to perform utility functions.

Choosing the appropriate method type helps in organizing code logically and maintaining clean class interfaces.

11. What is a decorator and why is it used?

A decorator is a function that takes another function as an argument, extends its behavior without explicitly modifying it, and returns the modified function. Decorators are commonly used for logging, enforcing access control and authentication, instrumentation, caching, and more.

They help in adhering to the DRY (Don't Repeat Yourself) principle by allowing the reuse of common functionality across multiple functions or methods.

12. How does the iteration protocol work in Python?

The iteration protocol in Python requires an object to implement the __iter__() method, which returns an iterator object. The iterator object must implement the __next__() method, which returns the next item in the sequence and raises a StopIteration exception when there are no more items.

This protocol allows objects to be used in loops and other contexts that require iteration, such as list comprehensions and generator expressions.

13. What is a context manager and why is with used?

A context manager is an object that defines the runtime context to be established when executing a with statement. It implements the __enter__() and __exit__() methods to set up and tear down resources, respectively.

The with statement ensures that resources are properly managed, and that cleanup code is executed, even if an error occurs within the block. This is commonly used for file operations, network connections, and threading locks.

14. How does the yield operator work, and how does it differ from return?

Answer:
yield is used within generator functions and returns a value, pausing the function's execution until the next call. It allows the function to maintain its state between calls, resuming execution from where it left off.

return completely terminates the function's execution and returns a value. After return, no further code is executed.

The main difference: yield is used to construct sequences without creating the entire structure in memory. This is especially useful when working with large datasets or streaming data.

yield turns a function into a generator, and its result becomes an iterator. This approach reduces memory consumption and enhances performance.

15. What are __init__, __str__, and __repr__, and how do they differ?

Answer:

  • __init__ — the object initializer, called when an instance of the class is created.
  • __str__ — defines the string representation of the object, returned by the str() function, and is used for displaying to the user.
  • __repr__ — returns the official string representation of the object, invoked by the repr() function, and is intended for debugging.

If __str__ is not defined, __repr__ will be used. A good practice is for __repr__ to return a string that can be used with eval() to recreate the object.

Not implementing these methods results in unreadable object outputs and complicates debugging. Overriding them is a standard practice in well-structured classes.

16. How do default arguments work in functions, and what error is associated with them?

Answer:
Default argument values in Python are evaluated once — at the time the function is defined, not each time it's called.

If a mutable object (like a list or dictionary) is used as a default value, it will retain changes between function calls.

This can lead to unexpected behavior where changes "accumulate." Instead, use None as the default and perform a check and initialization inside the function.

This error is one of the most common among developers unfamiliar with the interpreter's nuances.

17. What is property, and why is it used?

Answer:
property is a way to create managed access to a class attribute. It allows defining getter, setter, and deleter methods without changing the attribute access syntax.

It enables implementing protection, validation, or computed values when accessing fields, while maintaining simple syntax: obj.value instead of obj.get_value().

property is supported through the @property, @<property>.setter, and @<property>.deleter decorators, making the object's interface cleaner and more predictable.

This is an important encapsulation tool, especially during refactoring and increasing class logic complexity.

18. How does classmethod differ from staticmethod?

Answer:
@classmethod receives the class (cls) as its first argument and can access class attributes or create new instances. It's useful for implementing alternative constructors and class-wide logic.

@staticmethod doesn't receive cls or self. It's simply a function logically related to the class. It doesn't have access to the state of the class or its instances.

The difference lies in access and purpose: classmethod is needed for interacting with the class, while staticmethod is for logically grouping functions within the class.

19. What does the zip() function do, and how does it work?

Answer:
The zip() function combines multiple iterable objects into one, creating tuples with elements at the same positions.

It operates up to the minimum length among all input objects. The result of zip() is an iterator, not a list. This means it needs to be converted to a list or iterated over in a loop.

It's used for parallel processing of multiple collections, packing/unpacking data, forming dictionaries (dict(zip(keys, values))), and in analytics.

20. How does the __hash__ function work, and why must an object be immutable?

Answer:
The __hash__ method returns a numeric value used when storing the object in hash-based structures: dictionaries and sets.

For an object to be hashable, it must:

  • have a correct implementation of __hash__;
  • be immutable, so its hash doesn't change over time;
  • correctly implement __eq__, so objects with identical values have the same hash.

Violating these conditions leads to errors: the object may "disappear" from a dictionary or set, or be incorrectly grouped during comparisons.

Hashability is fundamental to the operation of dict, set, caching, and many algorithms, so implementing __hash__ should be thoughtful and consistent with __eq__.

Author of This Interview Questions

Marcus Johnson

Marcus Johnson

I've been working as a Senior Python Backend Developer at the company StreamFlow Analytics...

Learn more →