Interview questions · free

Python Developer interview questions and answers

29 questions Python Developer interviews in India ask, grouped by the 6 skills they test, with a short model answer for each. Then the HR round every job has. Try each one out loud before you open the answer.

  • 29 questions
  • 6 skills
  • 17 for freshers
  • Updated 30 Oct 2026

First mock interview free: 3 questions, a score and feedback on each · No card needed

Answer out loud first. Read the question, say your answer as you would in the room, then open the model answer and compare. Interviewers ask follow-ups, so learn the reasoning, not the words.

Python · Core skill

Core language

Python looks easy, so interviewers dig into how it really behaves, mutability, arguments, imports and errors, because that's where real bugs come from.

What is the difference between mutable and immutable types in Python? Why is a mutable default argument a problem?Fresher

An immutable object can't be changed after it's created: int, float, str, tuple, frozenset and bytes. Operations that look like changes create a new object. Mutable objects, such as list, dict and set, can be changed in place, and every variable pointing to them sees the change.

Default argument values are evaluated once, when the function is defined, not on each call. So def add_item(item, items=[]) shares one list across all calls, and items from earlier calls keep appearing. The fix is items=None, then inside the function, if items is None: items = []. Also note that a tuple holding a list is immutable itself, but the list inside it can still change.

What are *args and **kwargs? When would you use them?Fresher

A single star before a parameter, as in *args, collects any extra positional arguments into a tuple. Two stars, as in **kwargs, collect extra keyword arguments into a dict. For def log(message, *args, **kwargs), calling log("hi", 1, 2, level="info") gives args = (1, 2) and kwargs = {"level": "info"}. The names are only a convention; the stars do the work, and **kwargs must come last.

The same stars unpack when calling: f(*values) spreads a list into positional arguments, and f(**options) spreads a dict into keyword arguments. They're most useful in decorators and wrappers that pass arguments through to another function without knowing them in advance. In ordinary functions, explicit parameters are clearer and editors can check them.

What is a list comprehension, and when would you use a generator expression instead?Fresher

A list comprehension builds a list in one readable expression: [n * n for n in numbers if n % 2 == 0] gives the squares of the even numbers. There are dict and set comprehensions too, such as {user.id: user for user in users}.

A generator expression uses round brackets, (n * n for n in numbers), and produces values one at a time instead of building the whole list in memory. Use it when you only need to loop once or pass the values to a function such as sum(), any() or max(), or when the data is large, such as a big log file. A generator can be consumed only once. Avoid comprehensions with side effects or several nested loops; a normal loop is clearer there.

How do try, except, else and finally work together, and what's wrong with a bare except?1-6 years

The try block holds code that may fail, and except blocks catch specific exceptions. else runs only if no exception was raised, which keeps the try block small, holding just the risky line. finally always runs, whether or not there was an error, so it's for cleanup; often a with statement does that job better, as with open() for files.

A bare except: catches everything, including KeyboardInterrupt and SystemExit, so it can hide bugs and stop a program from being interrupted. Catch the specific exceptions you expect, such as ValueError or KeyError, or at most Exception, and log it. To convert an error, use raise PaymentError("failed") from e, which keeps the original cause in the traceback.

What happens when you import a module, and what does if __name__ == "__main__" do?1-6 years

Python searches the folders in sys.path for the module, runs its top-level code once, and stores the module in sys.modules. Later imports anywhere in the program reuse that cached module rather than running it again. A package is a folder of modules, usually with an __init__.py file.

When a file runs directly, its __name__ is "__main__"; when it's imported, __name__ is the module's name. So code under if __name__ == "__main__": runs only when you execute the file as a script, not when tests or other modules import it. A common follow-up is circular imports: two modules importing each other at the top level. Fix them by moving shared code into a third module.

Python · Core skill

Data structures

Choosing the right built-in data structure is most of writing good Python, and interviewers test it with small coding tasks and questions about speed.

What is the difference between a list and a tuple? When would you use a tuple?Fresher

A list is mutable: you can add, remove and change items. A tuple is immutable once created. Because of that, a tuple of immutable values is hashable, so it can be a dictionary key or a set member, for example a (latitude, longitude) pair as a key in a cache. Tuples also use slightly less memory.

Use a tuple for a fixed group of values that belong together, such as a record or a function returning several values, return name, age, and a list for a collection of similar items that may grow. A common trick question: a one-item tuple needs a trailing comma, (5,), because (5) is just the number 5. For readable records, use a namedtuple or a dataclass.

How would you remove duplicates from a list while keeping the original order?Fresher

The shortest way is list(dict.fromkeys(items)). Dictionary keys are unique, and since Python 3.7 dictionaries keep insertion order, so the first occurrence of each item stays in place. It's O(n). Converting to a set, list(set(items)), also removes duplicates, but loses the order.

The explicit way, which interviewers sometimes ask you to write, is a loop with a seen set: for each item, if it isn't in seen, add it to seen and append it to the result. Both need hashable items. For unhashable items, such as dictionaries, track a hashable key instead, for example each record's email address.

How do you sort a dictionary by its values, for example to find the top three scorers?Fresher

Dictionaries don't sort themselves, but sorted() works on their items: sorted(scores.items(), key=lambda pair: pair[1], reverse=True) returns a list of (name, score) tuples from highest to lowest, and slicing with [:3] gives the top three. Passing that list to dict() gives a dictionary in that order, since dictionaries keep insertion order. For ties, sort by a tuple, key=lambda pair: (-pair[1], pair[0]), which puts higher scores first and names alphabetically within the same score.

For only the top few from a large dictionary, heapq.nlargest(3, scores.items(), key=lambda pair: pair[1]) avoids sorting everything. If you're counting first, Counter's most_common(3) does both.

What is the time complexity of x in list compared with x in set? Which list operations are slow?1-6 years

Checking membership in a list is O(n), since Python compares items one by one. In a set or dict it's O(1) on average, because the hash takes you straight to the right slot; it degrades only with many collisions. So if you check many values against a large collection, convert it to a set once: checking m values against n items drops from O(n × m) to about O(n + m).

For lists, append() and pop() from the end are O(1) amortised, and indexing is O(1). But insert(0, x), pop(0) and removing from the middle are O(n), because every later item shifts. For a queue, use collections.deque, which adds and removes at both ends in O(1). Sorting is O(n log n).

Which tools from the collections module have you used? Give a real use for Counter, defaultdict and deque.1-6 years

Counter counts hashable items: Counter(words).most_common(5) gives the five most frequent words in a log or a survey, and counters can be added and subtracted. defaultdict creates a default value for a missing key, so grouping becomes one line: groups = defaultdict(list), then groups[order.city].append(order), with no check for whether the key exists.

deque is a double-ended queue with O(1) appends and pops at both ends, which suits a breadth-first search queue. With maxlen, deque(maxlen=100) keeps only the last 100 items, such as recent requests. namedtuple gives lightweight, readable records. OrderedDict is less needed now that dicts keep order, but its move_to_end() helps when building a simple LRU cache.

Python

OOP

Python projects, from Django models to automation scripts, use classes everywhere, and interviewers check you understand Python's own way of doing OOP.

What is the difference between an instance method, a class method and a static method?Fresher

An instance method takes self, the object it's called on, and works with that object's data; it's the normal kind. A class method, marked @classmethod, takes cls, the class itself, so it can create objects or use class-level data. Its classic use is an alternative constructor, such as User.from_dict(data), and because it receives cls, it creates the right type when called on a subclass. A static method, marked @staticmethod, receives neither; it's a plain function placed inside the class because it belongs there logically, like a validation helper.

If a static method doesn't relate to the class at all, a module-level function is often simpler.

What are dunder methods? Which ones have you implemented, and why?Fresher

Dunder (double underscore) methods let your objects work with Python's built-in syntax and functions. __init__ sets up a new object. __repr__ gives an unambiguous representation for developers and logs, and __str__ a readable one for users. __eq__ defines ==, and __lt__ lets objects be sorted. __len__ and __iter__ make an object work with len() and for loops, and __enter__ and __exit__ make it usable in a with statement.

One detail interviewers like: if you define __eq__ without __hash__, Python sets __hash__ to None, so your objects can no longer go into a set or be dictionary keys. Define __hash__ from the same fields as __eq__, or use a frozen dataclass.

When would you use a dataclass, and how do you add validation or a computed attribute to one?1-6 years

Use @dataclass for classes that mainly hold data. It generates __init__, __repr__ and __eq__ from the type-annotated fields, removing a lot of boilerplate. frozen=True makes instances immutable and hashable, and order=True adds comparisons. For a mutable default, use field(default_factory=list), never a bare [].

For validation, define __post_init__, which runs after the generated __init__: raise ValueError there if, say, quantity is negative. For a computed attribute, add a @property, such as total returning price times quantity, so it's always up to date. Dataclasses don't check types at runtime; to validate external input like API requests, a library such as Pydantic is the better fit.

How does super() work with multiple inheritance? What is the MRO?1-6 years

The MRO, method resolution order, is the order in which Python searches classes for a method. It's computed with the C3 algorithm, so each class appears before its parents and the order of base classes in the class statement is respected. You can see it with ClassName.__mro__.

super() doesn't simply mean "my parent": it means the next class in the MRO of the object's actual class. In a diamond, where B and C both inherit from A and D inherits from both, D's MRO is D, B, C, A, object. If every class calls super().__init__() and passes its remaining keyword arguments along, each runs exactly once. Calling A.__init__(self) directly instead can run A twice or skip C. This is how mixins in Django work.

Testing

Unit testing

Python teams rely on tests to catch errors a compiler would catch elsewhere, and interviewers expect you to use pytest and mock outside services confidently.

How do you write and run a unit test with pytest? How is it different from unittest?Fresher

In pytest a test is a plain function whose name starts with test_, in a file named test_*.py, using a normal assert: def test_discount(): assert apply_discount(1000, 10) == 900. Run pytest from the project folder; it finds the tests itself. When an assert fails, pytest shows the values on both sides. Useful options are -k to run tests matching a name, -x to stop at the first failure and -v for detail.

unittest is in the standard library and uses classes that inherit from TestCase, with methods like self.assertEqual() and setUp() and tearDown(). pytest needs less code, has fixtures and plugins, and can run existing unittest tests too, so most projects use it.

What are pytest fixtures and parametrize? Give an example of each.Fresher

A fixture provides something a test needs, such as a test client, sample data or a temporary database. You write a function with @pytest.fixture, and any test that names it as a parameter receives its value. Code after a yield in the fixture runs afterwards as cleanup. A scope such as module or session shares an expensive setup between tests, and fixtures in conftest.py are available to every test in that folder. Built-in fixtures include tmp_path and monkeypatch.

@pytest.mark.parametrize runs one test with many inputs: @pytest.mark.parametrize("email, valid", [("a@b.com", True), ("", False), ("no-at-sign", False)]). Each case is reported separately, which makes edge cases cheap to add.

Your function calls an external payment API with requests. How do you test it without calling the real API?1-6 years

Replace the network call with a mock. With unittest.mock.patch, or pytest-mock's mocker fixture, patch the name where your code looks it up: if payments.py does from requests import post, patch "payments.post", not "requests.post". Set what it returns, mock_post.return_value.status_code = 200 and mock_post.return_value.json.return_value = {"status": "captured"}, then assert your function's result and check the call with mock_post.assert_called_once_with(...).

Test the failures too: side_effect=requests.Timeout, a 500 response and malformed JSON. A cleaner design is to wrap the API in a small client class and pass it in, so tests use a fake. Libraries such as responses can fake HTTP at a lower level.

Reading an answer isn't the same as giving one

In a mock interview you answer new Python Developer questions out loud or typed, and every answer is scored, with what to fix and a stronger answer.

Try it free

Databases

SQL

Python developers work with databases through Django's ORM, SQLAlchemy or raw SQL, and interviewers still expect you to write and reason about plain SQL.

What is the difference between UNION and UNION ALL?Fresher

Both combine the results of two or more SELECT queries into one result. UNION removes duplicate rows, which means the database has to sort or hash the combined rows, so it's slower. UNION ALL keeps every row, duplicates included, and is faster.

The queries must return the same number of columns with compatible types, and the column names come from the first query. For example, combining current and archived orders into one report: if an order can't be in both tables, use UNION ALL, since removing duplicates would only cost time. Use UNION only when duplicates are possible and you really don't want them. Interviewers often follow up with how ORDER BY applies: once, to the combined result.

Write a query to find duplicate email addresses in a users table. How would you keep only one row for each email?Fresher

To find them: SELECT email, COUNT(*) FROM users GROUP BY email HAVING COUNT(*) > 1. If emails differ only by case, group by LOWER(email) instead.

To keep the oldest row for each email and delete the rest, number the rows within each email with a window function: DELETE FROM users WHERE id IN (SELECT id FROM (SELECT id, ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn FROM users) AS t WHERE rn > 1). The extra inner query is needed in MySQL, which doesn't allow deleting from a table you select from directly. Run the SELECT part first to check what will go, take a backup, then add a unique constraint so duplicates can't come back.

What is a correlated subquery? How would you rewrite one that is slow?1-6 years

A correlated subquery refers to a column of the outer query, so in principle it runs once for every outer row. For example, employees who earn more than their department's average: SELECT e.name FROM employees e WHERE e.salary > (SELECT AVG(salary) FROM employees WHERE dept_id = e.dept_id).

To make it faster, calculate each department's average once and join: WITH dept_avg AS (SELECT dept_id, AVG(salary) AS avg_salary FROM employees GROUP BY dept_id) SELECT e.name FROM employees e JOIN dept_avg d ON d.dept_id = e.dept_id WHERE e.salary > d.avg_salary. A window function, AVG(salary) OVER (PARTITION BY dept_id), also works. Many optimisers rewrite such queries themselves, so compare both with EXPLAIN before claiming a speed-up.

Web frameworks

REST APIs

Many Python developer jobs are backend roles building APIs with Django REST Framework, FastAPI or Flask, so expect design questions as well as framework details.

How would you design REST endpoints for a library app with books and loans?Fresher

Use nouns for resources, plural, and let the HTTP method say the action. GET /books lists books, with filters like ?author=...; GET /books/{id} returns one; POST /books creates one and returns 201 Created; PUT or PATCH /books/{id} updates; DELETE /books/{id} removes it and returns 204 No Content. Loans are their own resource: POST /loans to borrow a book, and GET /members/{id}/loans for a member's loans.

Use status codes consistently: 400 or 422 for invalid input, 401 when not logged in, 403 when not allowed, 404 when it doesn't exist, and 409 when the book is already on loan. Avoid verbs in URLs like /getBook or /borrowBook.

What is serialisation in a Python API, and how do Django REST Framework and FastAPI handle it?Fresher

Serialisation turns Python objects, such as model instances, datetime and Decimal values, into JSON for the response; deserialisation turns incoming JSON back into validated Python data. The standard json module can't handle datetime or Decimal on its own, which is one reason frameworks do this for you.

In Django REST Framework, a Serializer or ModelSerializer declares the fields; for input you call serializer.is_valid() and then use serializer.validated_data. In FastAPI, you declare Pydantic models as type hints: the request body is parsed and validated automatically, with a 422 response on errors, and response_model controls exactly which fields are returned, so internal fields such as a password hash never leak.

An endpoint returns thousands of records. How would you add pagination, and which kind would you choose?1-6 years

Never return everything at once: cap the page size on the server, for example 50 by default and 100 at most. Offset pagination, ?limit=50&offset=100, is simple and lets users jump to a page number, but large offsets get slow because the database still reads and skips the earlier rows, and new inserts can shift items between pages.

Cursor, or keyset, pagination asks for rows after the last one seen: WHERE id > :last_id ORDER BY id LIMIT 50, using an index. It stays fast at any depth and is stable while data changes, which suits feeds and large exports, but it can't jump to page 40. Return the next cursor or link in the response, as DRF's pagination classes do.

How would you add token-based authentication to a Python REST API?1-6 years

Clients log in once with their credentials and receive a token, then send it in the Authorization: Bearer header on every request. With JWTs, the server checks the signature and expiry on each request without a database lookup; keep access tokens short-lived, minutes rather than days, and use a refresh token to get new ones. In DRF this is usually a package like Simple JWT, and in FastAPI a dependency that decodes and checks the token.

Return 401 when the token is missing or invalid, and 403 when the user is authenticated but not allowed. Use HTTPS only, keep the signing key in an environment variable, and never put sensitive data in a JWT's payload: it's encoded, not encrypted.

Every job

HR and behavioural round

Python is used for web, data and automation, so the HR round checks why you chose it, where you want to grow, and how you learn and work in a team.

Tell me about yourself.Fresher

Spend about a minute and point everything towards the Python role. Present, past, future: "I'm a [degree] graduate from [college], and I enjoy building backend services and automating repetitive work in Python. In my final-year project I built [project] with [Django/FastAPI] and PostgreSQL, where I wrote the [API and authentication] and [one result]. I also [internship, open-source contribution or a script that saved time somewhere]. I'm looking to start as a Python developer where I can write production code and learn good engineering practice."

Choose the project you can explain best; the next questions will dig into it.

Python is used for web development, data and automation. Which direction do you want to grow in, and why this role?Fresher

Show that you've thought about it and that it matches the job. For a backend role: "I enjoy building things people use, so backend development with Python is where I want to grow. My project [name] was a Django REST API, and I liked designing the data model and the endpoints. This role works on [what the job description mentions], which fits that directly." You can mention interest in data or automation as a strength, but don't make it sound as if you'd rather be doing something else.

Read the job description carefully before answering. Interviewers want to see that you've chosen this role, not just any Python job.

How do you learn a new library or framework quickly?Fresher

Describe a real method and prove it with an example. A good structure: read the official tutorial and docs first, build something small and real with it, then read other people's code and the parts of the docs you skipped. For example: "When my project needed FastAPI, I worked through its official tutorial in two days, then rebuilt one of our Flask endpoints with it, including tests. When I got stuck, I read the source or the GitHub issues instead of copying answers I didn't understand."

Interviewers like hearing that you learn from the official documentation and by building, and that you ask for help after trying yourself.

Tell me about a time you automated something that saved your team time.1-6 years

Python developers are often valued for this, so have a story ready with a measurable result. Use STAR: "Every Monday, a teammate spent about three hours merging sales reports from five spreadsheets (situation). I offered to automate it (task). I wrote a Python script with pandas that read the files, cleaned the columns, merged them and emailed the summary, and scheduled it to run every Monday morning (action). It now takes two minutes, and it caught two data errors in the first month that had gone unnoticed (result)."

Mention how you made it reliable: error handling, logging and documentation so others could run it.

How do you handle code review comments you disagree with?1-6 years

Show that you separate the code from the person, and decide with reasons. Structure: first understand the comment, then explain your view with evidence, then accept the team's decision. For example: "A reviewer asked me to replace a simple loop with a complex comprehension. I asked why, and explained that the loop was easier to read for newer team members. We agreed on the loop, and noted the convention in our style guide. Other times the reviewer was right, and I've learnt a lot that way."

Never take review comments personally or argue at length in comments; a short call often settles it faster.

Free interview questions, roadmaps and mock interviews: https://myskillgrid.com/interview-questions/python-developer/

Ready to answer them for real?

Your first Python Developer mock interview is free: 3 questions, a score and feedback for each, and a stronger answer to learn from, in about 10 minutes.