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: August 28, 2026)

51. What do any() and all() do?Language SpecificEasy

Question Details

Explain their Boolean aggregation behavior, short-circuiting, and the results for empty iterables.

Short Interview Answer (30-60 seconds)

any() returns True when at least one item is truthy. all() returns True only when every item is truthy. Both stop as soon as the result is known. For an empty iterable, any() returns False and all() returns True.

Detailed Explanation

Use any() when one truthy result is enough. Use all() when every result must be truthy. Each function accepts an iterable and applies normal Python truth testing to its items. Values such as False, None, zero, an empty string, and an empty collection are falsy. Other objects are usually truthy unless their type defines different truth behavior.

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?

any() returns True as soon as it reads one truthy item. It returns False if every item is falsy or the iterable is empty. all() returns False as soon as it reads one falsy item. It returns True if every item is truthy. It also returns True for an empty iterable because no falsy item breaks the condition.

This early stopping is called short circuit evaluation. If the result is found after k items, the time cost is proportional to k. In the worst case, every item is checked. The functions use constant extra memory while iterating, not counting the iterable itself. With an infinite iterable, a call may never finish if no decisive item appears. Truth testing can also raise an exception if an object defines faulty truth behavior. Use these functions for clear condition checks, but not when you need the matching item, the failing item, or a count.

What do any() and all() do? diagram
Where it is used

any() is useful when checking whether at least one permission is granted, one search result matches, one feature flag is enabled, or one health check succeeds. all() is useful when confirming that every required field is valid, every dependency check succeeds, or every value meets a rule. They work well with generator expressions because values can be produced one at a time and evaluation can stop early. In production code, handle empty input separately when the business rule requires at least one item.

Why Interviewers Ask This

Interviewers ask this question to check whether a candidate understands truth testing, Boolean aggregation, short circuit evaluation, empty iterable behavior, and the effect of lazy iteration. It also tests whether the candidate can choose the correct function for validation and production condition checks.

Common interview mistakes

A common mistake is reversing the meanings of any() and all(). Another mistake is thinking they accept only literal True and False values, even though they apply Python truth testing to every item. Developers may incorrectly expect all() on an empty iterable to return False. Another mistake is creating a complete list before the call when a generator expression could reduce memory use and avoid unnecessary work. It is also incorrect to use these functions when the program must return the exact item that passed or failed.

Interview tip

Begin with the direct rule. Say that any() needs one truthy item, while all() needs every item to be truthy. Then mention short circuit evaluation and the empty iterable results. These details show that you understand both the visible result and the runtime behavior.

Interviewer may ask next
Why does all() return True for an empty iterable?

It returns True because the empty iterable contains no falsy item that violates the requirement. all() checks whether every observed item is truthy, and there is no counterexample. This matters in validation code because all() alone does not prove that at least one item exists. When at least one item is required, the program must check for that condition separately.

Why can a generator expression be better than a list with any() or all()?

A generator expression can produce one value at a time, so any() or all() can stop before evaluating every possible value. This can reduce extra memory use and unnecessary computation. The tradeoff is that a generator is consumed as it is read, and the call may never finish for an infinite generator when no decisive value appears.

52. How do lists and tuples differ?Language SpecificEasy

Question Details

Compare their mutability, syntax, supported operations, hashability, memory characteristics, and typical use cases.

Short Interview Answer (30-60 seconds)

The main difference is that a list is mutable, while a tuple is immutable. A list can add, remove, replace, or reorder items after creation. A tuple cannot replace, add, or remove its stored item references. Lists normally use square brackets and are best for collections that change. Tuples use commas, often inside parentheses, and are best for fixed groups of values. Lists are not hashable. A tuple can be hashable when every item inside it is hashable. In CPython, tuples also usually use less memory than lists with the same items.

Detailed Explanation

See the Code while reading this explanation.

Use a list when the collection must change. Use a tuple when the collection represents a fixed group of 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?

A list is mutable. Mutable means the same list object can be changed after it is created. You can append an item, remove an item, replace an item by index, reverse the list, or sort it in place.

A tuple is immutable. Immutable means Python does not allow you to replace, add, or remove the item references stored in that tuple after creation. A tuple therefore has no append, extend, remove, pop, reverse, or sort method.

A list normally uses square brackets. For example, tasks = ["read", "test"]. A tuple is created by commas, although parentheses are commonly used for clarity. For example, point = (10, 20). The comma is important. A one item tuple must include a trailing comma. single = (10,) is a tuple, while single = (10) is the integer 10.

Lists and tuples support many of the same read operations. Both support indexing, slicing, iteration, len, membership checks with in, count, index, concatenation, and repetition. Concatenation requires matching sequence types. A list can be concatenated with another list. A tuple can be concatenated with another tuple.

Lists also support operations that change the existing object. Tuples do not. An expression such as values = values + (30,) does not change the old tuple. It creates a new tuple and makes the variable values refer to the new object.

A list is not hashable, so it cannot be a dictionary key or a set element. Hashable means an object has a hash value that remains suitable for lookup during its lifetime. A tuple can be hashable, but only when every item inside it is hashable. For example, (10, 20) can be a dictionary key. A tuple such as ([10], 20) cannot be a dictionary key because the inner list is not hashable.

Tuple immutability is shallow. The tuple cannot replace an item reference, but a mutable object stored inside the tuple can still change. For example, data = ([1, 2], "ready") cannot assign a new value to data[0]. However, data[0].append(3) is allowed because that operation changes the inner list, not the tuple structure.

Assignment does not copy a list or a tuple. If two variables refer to the same list, a mutation through one variable is visible through the other. Two variables can also refer to the same tuple, but the tuple structure cannot be mutated. Creating a separate list requires an explicit copy when independent mutation is needed.

In CPython, a tuple usually uses less memory than a list containing the same item references. A list commonly keeps extra capacity so later append operations can be efficient. A tuple has a fixed size and does not need growth capacity. Exact memory use and small speed differences depend on the Python implementation and version, so type choice should be based mainly on behavior and meaning.

In production code, lists are useful for active tasks, collected records, request results, validation messages, and other collections that change. Tuples are useful for coordinates, fixed return values, compound dictionary keys, and records whose positions have stable meaning. A tuple also communicates intent by showing that the group is not expected to change.

How do lists and tuples differ? diagram
Key Insight / Why This Solution Works

First, decide whether the collection must change after creation. Use a list when items must be added, removed, replaced, reordered, or sorted in place.

Second, decide whether the values form one fixed record. A coordinate such as (10, 20) is a good tuple because each position has a stable meaning.

Third, check whether the value must be used as a dictionary key or set element. A list cannot be used. A tuple can be used only when every item inside it is hashable.

Fourth, inspect any nested values. A tuple does not make an inner list, dictionary, or set immutable.

Fifth, check whether assignment or copying matters. Assignment creates another reference to the same object. Make an explicit copy when two lists must change independently.

Finally, consider memory and small runtime differences only after choosing the correct behavior. In CPython, tuples are usually smaller, while lists provide the flexibility required for changing data.

Example

The code creates a list named tasks and a tuple named point. It changes the list by appending an item and replacing the first item. It reads values from the tuple without changing its structure. It then uses the tuple as a dictionary key because both integers inside it are hashable. Finally, it creates a tuple containing a list and changes the inner list. This demonstrates that tuple immutability is shallow. The tuple still refers to the same inner list, but that list can mutate.

Code
def main():
    tasks = ["read", "test"]
    point = (10, 20)

    tasks.append("deploy")
    tasks[0] = "review"

    print("Changed list:", tasks)
    print("Fixed tuple:", point)
    print("First coordinate:", point[0])

    locations = {point: "office"}
    print("Tuple dictionary key:", locations[(10, 20)])

    grouped_data = ([1, 2], "ready")
    grouped_data[0].append(3)
    print("Tuple with changed inner list:", grouped_data)


if __name__ == "__main__":
    main()
Where it is used

Lists are used for collections that change while a program runs. Examples include active jobs, API results collected over time, validation errors, user selections, queued work, and records that must be reordered or updated. Tuples are used for fixed groups of values. Examples include coordinates, dimensions, color components, fixed return values, database result rows, and values whose positions have stable meaning. A tuple is also useful as a compound dictionary key when every item inside it is hashable. For example, a cache can use (user_id, page_number) as one key. A tuple can communicate intent to other developers. It shows that the collection structure should remain fixed. This does not provide deep immutability, so mutable objects inside the tuple still require care.

Why Interviewers Ask This

Interviewers ask this question to check whether a candidate understands core Python data structures and can choose the right structure for changing or fixed data. They are also testing knowledge of mutation, syntax, available operations, hashability, copying, memory use, and the effect of sharing an object between different parts of a program.

Common interview mistakes

A common mistake is saying that a tuple and everything inside it are immutable. A tuple can contain a list, dictionary, set, or another mutable object. The tuple cannot replace that object, but the inner object can still change.

Another mistake is assuming every tuple is hashable. A tuple is hashable only when every item inside it is hashable. A tuple containing a list is not hashable.

Some developers forget the comma in a one item tuple. The expression (10) is an integer. The expression (10,) is a tuple.

Another mistake is believing tuple concatenation changes the existing tuple. It creates a new tuple and rebinds the variable.

It is also incorrect to assume assignment creates a copy. After second = first, both variables refer to the same object. This is especially important with lists because a mutation through one reference is visible through the other.

Another mistake is claiming that tuples always provide a meaningful performance improvement. Memory use and speed depend on the Python implementation and version. The main reason to choose a tuple is fixed structure and clear intent.

Finally, using a tuple for data that must change often can create repeated allocations because every structural change requires a new tuple.

Interview tip

Begin with mutability because it is the main difference. Then compare syntax, supported operations, hashability, memory behavior, and common use cases. Mention that tuple immutability is shallow and that a tuple is hashable only when all of its items are hashable. Finish with the practical rule: use a list for changing collections and a tuple for fixed groups of values.

Interviewer may ask next
Can a tuple change when it contains a list?

Yes, the inner list can change, but the tuple cannot replace that list reference. Tuple immutability is shallow. It protects the tuple structure, not mutable objects stored inside it. This matters because the changing inner list can affect program state, and the tuple is not hashable while it contains that list.

Should a tuple always replace a list to save memory?

No, use a tuple only when the collection is logically fixed. In CPython, a tuple usually uses less memory than a list containing the same item references, but the exact difference depends on the implementation and version. The tradeoff is that lists support efficient mutation, while changing a tuple structure requires creating a new tuple. Correct behavior and clear intent matter more than a small memory saving.

53. How do dictionaries and sets differ?Language SpecificEasy

Question Details

Compare what each structure stores, uniqueness rules, membership behavior, ordering guarantees, supported operations, and typical use cases.

Short Interview Answer (30-60 seconds)

A dictionary stores unique keys that map to values. A set stores unique members without attached values. I use a dictionary when I need to look up data by a key, such as a user ID mapped to a user name. I use a set when I need uniqueness, fast membership checks, or group operations. Membership in a dictionary checks keys, while membership in a set checks its members.

Detailed Explanation

See the Code while reading this explanation.

The practical decision is simple. Use a dictionary when one item must point to another item. Use a set when you only need unique members.

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 dictionary stores key and value pairs. For example, {101: "Asha", 102: "Sam"} maps each user ID to a user name. Dictionary keys are unique. If code assigns a new value to an existing key, Python replaces the value connected to that key.

A set stores members without attached values. For example, {101, 102} records which user IDs were visited. Set members are unique. Adding 101 again does not create a second 101.

Membership behavior is different. The expression 101 in users checks whether 101 is a key in the users dictionary. It does not search the dictionary values. To search values, code can use "Asha" in users.values(), but that normally requires checking values one by one. The expression 101 in visited checks whether 101 is a member of the visited set.

Dictionary keys and set members must be hashable. Hashable means Python can calculate a hash value that remains stable while the item is stored. Integers, strings, and tuples containing only hashable items are common examples. Lists, sets, and dictionaries are mutable and cannot be used directly as dictionary keys or set members.

Dictionaries preserve insertion order. Iteration visits keys in the order they were first inserted. Changing the value for an existing key does not move that key to a new position. Removing a key and adding it again places it at the end. Sets do not guarantee insertion order or any other useful business order. Code should not depend on the displayed or iteration order of a set.

Dictionaries support key lookup, value assignment, updates, deletion, and access to keys, values, and item pairs. Sets support membership checks, adding members, removing members, and group operations such as union, intersection, difference, and symmetric difference.

Some values that compare as equal may act as the same key or member. For example, True and 1 compare as equal and have the same hash. A dictionary cannot keep them as two separate equal keys, and a set cannot keep them as two separate equal members.

Use a dictionary for records indexed by ID, configuration data, counters, caches, lookup tables, and grouped results. Use a set for duplicate removal, visited items, permissions, membership checks, and comparisons between groups.

Both structures use hash table storage and keep extra capacity to support fast operations. Their exact memory use depends on the Python implementation, Python version, collection size, and stored objects. A dictionary stores references for both keys and values. A set stores member references without separate mapped values. It is not safe to promise an exact memory ratio between them.

An empty dictionary is written as {}. An empty set must be written as set(). The expression {} never creates an empty set.

How do dictionaries and sets differ? diagram
Key Insight / Why This Solution Works

First, decide whether each stored item needs an attached value. If it does, use a dictionary.

Second, if no attached value is needed, decide whether uniqueness or group comparison is important. If it is, use a set.

Third, identify what membership should mean. In a dictionary, membership checks keys. In a set, membership checks members.

Fourth, confirm that every dictionary key or set member is hashable. Do not use a list, set, or dictionary directly in either position.

Fifth, decide whether insertion order matters. A dictionary preserves insertion order. A set does not provide an order that application logic should depend on.

Finally, choose operations that match the task. Use dictionary access and assignment for mapped data. Use set union, intersection, difference, or membership checks for groups of unique items.

Example

The example uses the same user IDs to show the difference between the two structures. The users dictionary maps each user ID to a user name. Assigning a new value to key 101 changes the mapped name because dictionary keys are unique. The visited set stores only user IDs. Adding 101 again has no effect because set members are unique.

The membership examples show that dictionary membership checks keys. Searching dictionary values requires users.values(). The set membership example checks set members directly.

The example sorts sets only before printing them. Sorting gives deterministic display output, but it does not change the fact that sets themselves do not guarantee insertion order. The intersection, union, and difference operators create new sets without changing the original sets.

Code
def main():
    users = {
        101: "Asha",
        102: "Sam",
    }

    visited = {101, 102}

    users[101] = "Asha Patel"
    visited.add(101)

    print(users)
    print(sorted(visited))

    print(101 in users)
    print("Asha Patel" in users)
    print("Asha Patel" in users.values())
    print(101 in visited)

    active_users = {101, 103}

    common_users = visited & active_users
    all_users = visited | active_users
    visited_only = visited - active_users

    print(sorted(common_users))
    print(sorted(all_users))
    print(sorted(visited_only))


if __name__ == "__main__":
    main()
Where it is used

Dictionaries are used for user records indexed by ID, configuration settings, request data, counters, caches, lookup tables, grouped results, and mappings between names and objects. Sets are used for removing duplicate values, checking whether an item was already processed, storing permission names, tracking visited nodes, finding common values, and comparing groups. For example, a notification service can use a dictionary to map each user ID to a user name. It can use a set to track which user IDs have already received a notification.

Why Interviewers Ask This

Interviewers ask this question to check whether a candidate understands two core Python data structures and can choose the correct one for a real task. They are testing knowledge of stored data, uniqueness, membership checks, ordering, hashability, supported operations, performance, and memory tradeoffs.

Common interview mistakes

A common mistake is using {} for an empty set. This creates an empty dictionary. Use set() for an empty set.

Another mistake is assuming that value in my_dictionary searches dictionary values. It checks keys. Use value in my_dictionary.values() when value membership is required, and remember that this normally takes linear time.

Some developers depend on set iteration order because the order may appear stable in one run. Python does not guarantee a useful set order. Sort the members when output requires a predictable order.

Another mistake is using a list, set, or dictionary as a dictionary key or set member. These objects are mutable and not hashable.

A dictionary cannot contain duplicate equal keys. Assigning a value to an existing key replaces its mapped value. A set cannot contain duplicate equal members. Adding an equal member again has no effect.

Converting a list to a set removes duplicates, but it does not preserve a guaranteed original order. Use list(dict.fromkeys(items)) when duplicate removal and insertion order are both required.

Another mistake is assuming that equal values with different types must remain separate. For example, True and 1 act as the same dictionary key or set member because they compare as equal and have the same hash.

Interview tip

Start with the practical choice. Say that a dictionary stores unique keys mapped to values, while a set stores unique members only. Then explain membership behavior, ordering, hashability, one use case for each, and the average lookup cost.

Interviewer may ask next
Can a list be used as a dictionary key or set member?

No. A list cannot be used as a dictionary key or set member because it is mutable and not hashable. Python requires the hash of a stored key or member to remain stable. A tuple can be used only when every item inside the tuple is also hashable.

Which structure should be used to remove duplicates while preserving insertion order?

Use a dictionary based approach when insertion order must be preserved. The expression list(dict.fromkeys(items)) removes duplicates because dictionary keys are unique and dictionaries preserve insertion order. Using set(items) also removes duplicates, but set order is not guaranteed. The dictionary approach uses extra memory for a new dictionary and result list.

54. How do append(), extend(), and insert() differ for lists?Language SpecificEasy

Question Details

Explain how each method changes a list, what arguments it accepts, and how append differs from adding each element of another iterable.

Short Interview Answer (30-60 seconds)

append adds one object to the end of a list. extend takes an iterable and adds each item from it to the end. insert takes an index and one object, then places that object before the element currently at that position. All three methods change the existing list and return None. I use append for one object, extend for several items, and insert when a specific position is required.

Detailed Explanation

The practical rule is to use append for one object, extend for every item from an iterable, and insert for one object at a chosen position.

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?

append accepts one object and adds it to the end. Appending [3, 4] to [1, 2] produces [1, 2, [3, 4]]. The added list remains one nested object.

extend accepts one iterable. Python reads that iterable and adds each item separately. Extending [1, 2] with [3, 4] produces [1, 2, 3, 4]. A non iterable value raises TypeError. Extending with a string adds its characters separately.

insert accepts an index and one object. It places the object before the element at that position. A large positive index places it at the end. A very small negative index places it at the beginning.

All three methods modify the same list and return None. They store references to objects rather than copying the objects themselves. The list may allocate more internal space as it grows. append is usually constant time on average. extend takes time based on the number of added items. insert may take time based on the list size because existing references may need to move.

How do append(), extend(), and insert() differ for lists? diagram
Where it is used

append is useful when a program collects one result at a time, such as validation errors, parsed records, or processed file names. extend is useful when several results already exist in another iterable, such as records from another page or values produced by a generator. insert is useful when one value must appear at a specific position, such as adding a default choice at the start of a menu. In production code, repeated insertion near the beginning of a large list should usually be avoided because many existing references may need to move. These methods change the original list, so shared references to that list will observe the changes.

Why Interviewers Ask This

Interviewers ask this question to check whether a candidate understands how Python changes lists in place. It also tests whether the candidate can choose the correct method, predict the resulting list structure, reason about object references, and consider the cost of moving or adding elements.

Common interview mistakes

A common mistake is using append when each item from another iterable should be added separately. This creates one nested element instead of adding the items individually. Another mistake is writing values = values.append(3). append returns None, so values then refers to None. The same return behavior applies to extend and insert. Developers may also forget that extend with a string adds separate characters. Another mistake is expecting these methods to copy mutable objects. They add object references, so later changes to a shared mutable object can be visible through the list. Repeated insert calls near the beginning of a large list can also create unnecessary performance cost.

Interview tip

Start with the decision rule. Say that append adds one object, extend adds each item from an iterable, and insert adds one object at a chosen index. Then use the same small list example to show that append can create a nested list while extend adds separate items. Finish by stating that all three methods modify the original list and return None.

Interviewer may ask next
What happens when extend is called with a string or a non iterable value?

A string is iterable, so extend adds each character as a separate list element. For example, extending [1, 2] with "ab" produces [1, 2, "a", "b"]. A non iterable value, such as an integer, raises TypeError because extend needs an object that Python can iterate over. This matters because append should be used when the complete string or other object must remain one list element.

Why can insert be slower than append, and what memory behavior do these methods have?

insert can be slower because Python may need to move existing object references to create space at the requested position. The work can grow with the size of the list, especially near the beginning. append is usually constant time on average because it adds at the end, although the list may occasionally allocate a larger internal storage area. append, extend, and insert change the existing list and store references to the added objects. They do not copy those objects.

55. How do remove(), pop(), and del differ?Language SpecificEasy

Question Details

Explain removal by value versus index, return values, error behavior, and how del can also remove slices or delete a name.

Short Interview Answer (30-60 seconds)

Use remove when you know the value, pop when you know the index and need the removed value, and del when you want to delete an item, a slice, or a variable name. remove deletes the first matching value and returns None. pop removes and returns an item, using the last item when no index is given. del is a statement, so it does not return a value.

Detailed Explanation

The practical choice depends on what you know and whether you need the removed value. list.remove(value) searches from the start of the list and deletes the first item that compares equal to the value. It returns None. If no match exists, Python raises ValueError. Use it when the value matters more than its position.

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?

list.pop(index) removes and returns the item at that index. With no index, it removes the final item. A missing item in an empty list or an index outside the valid range raises IndexError. Negative indexes are allowed when they refer to a valid position.

del is a Python statement. del items[index] removes one item. del items[start:stop] removes a slice. del name removes that name from its current scope. It does not directly destroy the object if other references still point to it.

All list removal operations change the original list. remove may scan many items before finding a match. Removing from the front or middle also shifts later items left. pop() from the end is amortized constant time in normal Python list use. These operations usually do not create a new list, although Python manages the internal list storage and may keep some allocated capacity.

How do remove(), pop(), and del differ? diagram
Where it is used

remove is useful when deleting the first occurrence of a known value, such as removing a selected tag from a list. pop is useful in stack processing, undo logic, work processing, or any flow that needs the removed item. del is useful when deleting a known position, removing a range with a slice, clearing the entire list with del items[:], or removing a temporary variable name from the current scope.

Why Interviewers Ask This

Interviewers ask this question to check whether a candidate understands removal by value, removal by index, return values, exceptions, slice deletion, and variable name deletion. It also tests whether the candidate can choose a clear operation and explain the performance cost of changing a Python list.

Common interview mistakes

A common mistake is expecting remove to return the deleted item. It returns None. Another mistake is assuming remove deletes every matching value. It deletes only the first match. Developers may also call pop on an empty list or use an invalid index without handling IndexError. Another error is treating del like a function or expression that returns a value. del is a statement. It is also incorrect to assume del name always destroys the object immediately. It removes one name binding, but other references may still keep the object alive. Removing items while iterating over the same list can also skip values or produce confusing results because indexes change.

Interview tip

Start with the decision rule. Say remove is by value, pop is by index and returns the item, and del deletes an item, slice, or name without returning anything. Then mention the main exceptions and explain that removing from the middle of a list requires later items to shift.

Interviewer may ask next
What happens when remove cannot find the value?

Python raises ValueError because remove requires a matching value. This matters when absence is possible. Code can check whether the value is present or catch ValueError. Checking first may scan the list once and remove may scan it again, while catching the exception avoids that second scan when missing values are uncommon.

Which operation is best for repeatedly removing the last list item?

pop() is usually the best list operation because it removes and returns the final item and normally does not shift other elements. Its cost is amortized constant time. This makes a list suitable for stack behavior. Frequent removal from the front is linear time because remaining items must shift, so collections.deque is usually a better production choice when items must be removed often from both ends.

56. How do split() and join() work?Language SpecificEasy

Question Details

Explain how split creates substrings from a string and how join combines an iterable of strings using a separator, including common type errors.

Short Interview Answer (30-60 seconds)

split breaks one string into a new list of smaller strings. join combines an iterable of strings into one new string and places a separator between the items. For example, "red,green,blue".split(",") returns ["red", "green", "blue"], and ",".join(["red", "green", "blue"]) returns "red,green,blue". Every item passed to join must be a string, or Python raises TypeError.

Detailed Explanation

See the Code while reading this explanation.

Use split when you need to break one string into parts. Use join when you need to combine strings with a separator.

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?

For example, "red,green,blue".split(",") returns a new list containing "red", "green", and "blue". When a separator is provided, Python cuts at each occurrence of that exact separator. Repeated separators can create empty strings. For example, "red,,blue".split(",") returns ["red", "", "blue"]. An empty separator is invalid and raises ValueError.

When split is called without a separator, Python treats runs of whitespace as one separator and ignores whitespace at both ends. This differs from split(" "), which uses one exact space and can produce empty strings.

join works in the opposite direction. The separator calls the method. For example, ",".join(["red", "green", "blue"]) returns "red,green,blue". Every item must be a string. A non string item causes TypeError.

Strings are immutable, so neither method changes the original string. split creates a new list and new substring results. join creates a new result string. Time and memory use grow with the total amount of text processed.

How do split() and join() work? diagram
Example

The first example starts with the string "red,green,blue". split uses a comma as the separator and returns the list ["red", "green", "blue"]. join then uses the same comma separator and produces "red,green,blue". The second example contains integers. Each integer is converted to a string before join because join accepts string items only.

Code
def main():
    # Start with one string containing comma separated values
    text = "red,green,blue"

    # Split the string wherever a comma appears
    colors = text.split(",")
    print(colors)

    # Join the strings using a comma between each item
    combined = ",".join(colors)
    print(combined)

    # Start with integer values
    numbers = [10, 20, 30]

    # Convert each integer to a string before calling join
    number_text = ",".join(str(number) for number in numbers)
    print(number_text)


if __name__ == "__main__":
    main()
Where it is used

split is useful for simple command input, log lines, fixed separator records, configuration values, and user entered text. join is useful for display messages, file content, report rows, URL query fragments after proper encoding, and text created from validated values. split should not be used as a complete CSV parser when fields may contain quoted separators. Python's csv module should be used for that case. File system paths should normally be built with pathlib or os.path instead of plain string join.

Why Interviewers Ask This

Interviewers ask this question to check whether a candidate understands common Python string operations, separator rules, returned data types, empty values, whitespace handling, type errors, and object allocation. It also tests whether the candidate can choose safe text processing methods for production code.

Common interview mistakes

A common mistake is calling join on the iterable, such as values.join(","). The separator must call join, so the correct form is ",".join(values). Another mistake is passing integers, bytes, or other non string items directly to join, which raises TypeError. Developers may also assume split() and split(" ") behave the same, but repeated whitespace is handled differently. Other mistakes include using an empty separator, which raises ValueError, forgetting that repeated explicit separators can create empty strings, and using simple split for structured formats such as CSV that have quoting rules.

Interview tip

Explain split and join as opposite operations. State that split returns a new list, join returns a new string, the separator calls join, and every joined item must be a string. Then mention one edge case, such as repeated separators or whitespace handling.

Interviewer may ask next
What is the difference between split() and split(" ")?

split() without an argument treats any run of whitespace as one separator and ignores whitespace at the beginning and end. split(" ") uses one exact space as the separator, so repeated spaces can create empty strings. This matters when input may contain tabs, new lines, or inconsistent spacing.

What are the performance and memory costs of split and join?

Both operations take time proportional to the total amount of text they process. split creates a new list and substring results, so its memory use grows with the number and total size of the parts. join creates one new result string and must inspect every item before completing the result. Using a generator can avoid explicitly building a separate converted list in application code, but the converted strings and final output still require memory.

57. How do you raise an exception in Python?Language SpecificEasy

Question Details

Explain the raise statement, raising built-in or custom exception instances, re-raising the active exception, and preserving the original cause with exception chaining.

Short Interview Answer (30-60 seconds)

Use the raise statement when an operation cannot continue normally. I usually raise a specific built in or custom exception instance, such as raise ValueError("Age cannot be negative"). Inside an except block, raise by itself re raises the active exception and preserves its original traceback. When one error causes a new error, raise NewError(...) from original_error creates an explicit exception chain and preserves the original cause.

Detailed Explanation

See the Code while reading this explanation.

Use raise when an operation cannot return a valid result. The usual form is raise followed by an exception instance, such as raise ValueError("Age cannot be negative"). Python stops normal execution and searches outward for a matching except block. If no handler matches, Python ends the current program flow and prints a traceback.

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?

Use a built in exception when its meaning fits the problem. Use a custom exception that inherits from Exception when the failure belongs to your application domain.

Inside an except block, raise with no value re raises the active exception. This keeps the original exception type and traceback. Writing raise error instead raises that exception object again from the current location and can add another traceback frame.

When converting one exception into another, use raise NewError(...) from original_error. Python stores the original exception in the new exception's __cause__ attribute and shows both errors in the traceback. Python also keeps an implicit __context__ when a new exception is raised during exception handling. Use from None only when you intentionally want to hide that context from the displayed traceback.

Raising an exception creates traceback data and unwinds stack frames, so it is slower and uses more memory than normal branching. Exceptions should represent failures, not common control flow.

How do you raise an exception in Python? diagram
Example

The example validates an age value. A value that cannot be converted to an integer first causes TypeError or ValueError. The code converts that failure into a custom AgeInputError and uses explicit exception chaining to preserve the original cause. A negative integer raises a built in ValueError. The process_age function catches AgeInputError, adds simple logging, and uses bare raise to re raise the same active exception with its original traceback.

Code
class AgeInputError(Exception):
    """Raised when an age value cannot be converted to an integer."""


def parse_age(value):
    # Try to convert the supplied value into an integer.
    try:
        age = int(value)
    except (TypeError, ValueError) as original_error:
        # Raise a clearer custom exception for the application.
        # The from clause preserves the original conversion error.
        raise AgeInputError("Age must be a whole number") from original_error

    # Raise a built in exception when the value breaks the business rule.
    if age < 0:
        raise ValueError("Age cannot be negative")

    return age


def process_age(value):
    try:
        return parse_age(value)
    except AgeInputError:
        # A real application could record useful context in its logs here.
        print("The supplied age could not be parsed")

        # Re raise the active exception with its original traceback.
        raise


if __name__ == "__main__":
    examples = ["25", "unknown", "-3"]

    for example in examples:
        try:
            result = process_age(example)
            print(f"Valid age: {result}")
        except (AgeInputError, ValueError) as error:
            print(f"Error: {error}")
Where it is used

Exceptions are raised when validating API input, parsing configuration, checking file contents, enforcing business rules, or reporting failed database and network operations. Custom exceptions help callers handle domain failures without depending on low level implementation details. Exception chaining is useful when an application converts a parsing, database, or library error into a clearer application error while preserving the original cause for logs and debugging.

Why Interviewers Ask This

Interviewers ask this question to check whether a candidate understands how Python creates, propagates, and preserves errors. It also tests whether the candidate can choose a suitable exception type, define a custom exception, re raise an active exception correctly, and preserve the original cause for debugging.

Common interview mistakes

A common mistake is raising Exception for every failure instead of choosing a specific exception type. Another mistake is using bare raise when no exception is active, which causes RuntimeError. Developers may also write raise error when they mean to preserve the current traceback exactly, or replace a low level error without using the from clause, which makes the cause less clear. Other mistakes include catching exceptions too early, hiding unexpected errors with a broad except block, using unclear messages, and using exceptions for common branches that can be handled with normal conditions.

Interview tip

Explain three cases in order. First, show how to raise a specific built in or custom exception instance. Second, explain that bare raise re raises the active exception inside an except block. Third, show that raise new_error from original_error preserves the original cause. Mention that specific exception types and useful messages improve handling and debugging.

Interviewer may ask next
What happens if raise is used with no value when no exception is active?

It raises RuntimeError because there is no active exception to re raise. Bare raise depends on the current exception handling context and is normally used inside an except block. This matters because bare raise preserves an existing failure but cannot create a new application error by itself.

What is the difference between explicit exception chaining and raise from None?

Explicit chaining with raise NewError(...) from original_error stores the original exception in __cause__ and displays both failures. Using raise NewError(...) from None suppresses the previous exception context in the displayed traceback. This can make an error message cleaner, but it can also hide useful debugging information, so it should be used only when the original context is not helpful to the caller.

58. What are generators and iterators, and how are they different?Language SpecificMedium

Question Details

Explain the iterator protocol, iter(), next(), StopIteration, generator functions, yield, lazy evaluation, and why generators are useful when processing large or streaming datasets.

Short Interview Answer (30-60 seconds)

An iterator is any object that knows how to return its next value. It uses iter() and next(), and it stops by raising StopIteration. A generator is a special kind of iterator made by a function that uses yield. The main difference is that a generator creates values lazily, one at a time, without storing the whole result. That is useful for large files, streams, database rows, or pipelines where loading everything would waste memory.

Detailed Explanation

See the Code while reading this explanation.

The practical answer is that an iterator is the general protocol, and a generator is an easy way to create one. A protocol means a set of methods that Python expects. For iteration, Python expects an object to work with iter() and next().

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?

An iterator must return itself from __iter__(). It must also define __next__(), which returns the next value. When there are no more values, __next__() raises StopIteration. A for loop uses this behavior for you. It calls iter() once, then keeps calling next() until StopIteration happens.

A generator function is a function that contains yield. When you call it, Python does not run the full function immediately. It returns a generator object. That object is also an iterator. Each call to next() runs the function until the next yield. Then Python pauses the function and remembers its local state.

The key idea is lazy evaluation. Lazy evaluation means values are produced only when they are requested. This is different from building a full list first. A list stores every value in memory. A generator can produce one value, hand it to the caller, then continue later.

This matters when data is large or continuous. For example, reading a huge log file line by line is a good generator use case. The program does not need the whole file in memory. It only needs the current line and a little state.

The tradeoff is that generators are usually single pass. Once a value is consumed, it is gone unless you save it somewhere. If you need random access, repeated passes, or the total length, a list may be clearer. So I would use generators when I want memory efficiency and streaming behavior. I would use a list when the data is small or I need to reuse it many times.

What are generators and iterators, and how are they different? diagram
Key Insight / Why This Solution Works

First, explain the iterator protocol. An object becomes an iterator when iter() returns an object that has next() behavior through __next__(). Second, explain how the loop stops. When __next__() raises StopIteration, Python knows there are no more values. Third, explain a generator function. A function with yield creates a generator object instead of returning all values at once. Fourth, explain lazy evaluation. The generator produces one value only when next() asks for it. Fifth, choose between them. Use a custom iterator when you need full class control. Use a generator when yield gives the same behavior with less code.

Example

The code shows both forms. Countdown is a manual iterator because it implements __iter__() and __next__(). It raises StopIteration when the count reaches zero. countdown_generator does the same job with yield. Python saves the function state after each yield and resumes it on the next call. Both examples produce the same values, but the generator needs less manual code.

Code
class Countdown:
    def __init__(self, start):
        self.current = start

    def __iter__(self):
        return self

    def __next__(self):
        if self.current <= 0:
            raise StopIteration
        value = self.current
        self.current -= 1
        return value


def countdown_generator(start):
    current = start
    while current > 0:
        yield current
        current -= 1


print(list(Countdown(3)))
print(list(countdown_generator(3)))
Where it is used

Generators are used when processing large files, log streams, database result streams, message streams, and data pipelines. They are useful when each item can be handled one at a time. Iterators are also used in custom collection classes. For example, a class can define how its items should be visited without exposing its internal storage. In production code, generators often make pipelines simpler and reduce memory use.

Why Interviewers Ask This

Interviewers ask this to check whether you understand how Python loops work internally. They want to see if you know the iterator protocol, StopIteration, and lazy evaluation. They also want practical judgment. A good candidate knows when a generator saves memory and when a normal collection is simpler.

Common interview mistakes

A common mistake is thinking every iterable is an iterator. A list is iterable because iter(list) returns an iterator, but the list itself is not consumed by next() directly. Another mistake is forgetting StopIteration in a manual iterator. That can make code fail or loop forever. Some candidates think a generator runs when the function is called. It actually starts when next() asks for a value. Another mistake is trying to reuse a consumed generator like a list. If repeated passes are needed, store the values or create a new generator.

Interview tip

Start by saying that an iterator is the protocol and a generator is a convenient iterator made with yield. Then explain iter(), next(), StopIteration, and lazy evaluation. End with the practical use case. Generators are best when the data is large, streaming, or should be processed one item at a time.

Interviewer may ask next
What happens when a generator has already been consumed?

A consumed generator does not start over by itself. This behavior matters because a generator keeps its current position. When next() has already reached the end, the generator raises StopIteration. If you loop over it again, there are no values left. This is different from a list. A list can be iterated many times because the values are stored. If I need another pass over generated data, I would either create a new generator or save the values in a list. The tradeoff is memory. Saving values makes repeated reads easy, but it uses more memory.

When would you not use a generator?

I would not use a generator when I need random access, repeated passes, or the full length of the data. A generator is best for one pass processing. It gives each value when requested, but it does not naturally support indexing like items[5]. It also does not keep all previous values unless I store them myself. If the data is small and I need to sort it, count it, index it, or reuse it many times, a list is often simpler. The tradeoff is memory. A list uses more memory, but it gives easier access and repeated use.

59. 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.

60. What is duck typing?Language SpecificMedium

Question Details

Explain behavior-based compatibility, how Python code relies on supported operations rather than declared inheritance, and how protocols or abstract base classes can document expected behavior.

Short Interview Answer (30-60 seconds)

Duck typing means Python code usually cares about what an object can do, not which class it belongs to. If an object supports the operation my code needs, I can use it. For example, a function can call write on any object that provides a compatible write method. This makes code flexible, but the expected behavior should still be documented and tested because missing or incompatible operations fail when they are used.

Detailed Explanation

See the Code while reading this explanation.

Duck typing means code accepts an object because it supports the behavior the code needs. The object does not have to inherit from one required class. For example, a function that calls writer.write can work with a file object, an in memory stream, or a custom writer. They are compatible because each provides a suitable write method.

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?

Python normally checks this when the operation runs. It looks up write on the object. If the method exists, accepts the supplied arguments, and behaves as expected, the call works. If write is missing, Python raises AttributeError. If its call signature is incompatible, Python can raise TypeError.

Duck typing is useful for reusable functions, testing, adapters, iterables, context managers, and file like objects. Its main limitation is that the required behavior can be unclear. A Protocol can document the expected methods for static type checkers without forcing inheritance. An abstract base class is useful when explicit inheritance, shared code, or runtime membership rules are needed.

Duck typing itself does not copy the object or create a replacement object. Its cost is the normal attribute lookup and method call. In production, use small interfaces, clear names, type hints, focused tests, and useful boundary validation.

What is duck typing? diagram
Example

The example defines save_message without checking the concrete class of its argument. The function only calls write because that is the behavior it requires. FileWriter and MemoryWriter are unrelated classes, but both work because each provides a compatible write method. BrokenWriter does not provide write, so Python raises AttributeError when the call is attempted. The Writer Protocol documents the expected method for static type checking. It does not change the runtime method call or force the classes to inherit from Protocol.

Code
from typing import Protocol


class Writer(Protocol):
    # Any compatible object must provide this method.
    def write(self, text: str) -> None: ...


class FileWriter:
    # This class satisfies the expected behavior.
    def write(self, text: str) -> None:
        print(f"File output: {text}")


class MemoryWriter:
    def __init__(self) -> None:
        # Store written values in memory.
        self.messages: list[str] = []

    # This class also satisfies the expected behavior.
    def write(self, text: str) -> None:
        self.messages.append(text)
        print(f"Memory output: {text}")


class BrokenWriter:
    # This class does not provide a write method.
    def read(self) -> str:
        return "Nothing was written"


def save_message(writer: Writer, message: str) -> None:
    # The function uses behavior instead of checking a class name.
    writer.write(message)


def main() -> None:
    file_writer = FileWriter()
    memory_writer = MemoryWriter()

    # Both unrelated classes work because both provide write.
    save_message(file_writer, "Order saved")
    save_message(memory_writer, "Order saved")

    print(memory_writer.messages)

    broken_writer = BrokenWriter()

    try:
        # This fails at runtime because write is missing.
        save_message(broken_writer, "Order saved")
    except AttributeError as error:
        print(f"Runtime error: {error}")


if __name__ == "__main__":
    main()
Where it is used

Duck typing is used when one function should work with several kinds of objects that provide the same operation. A logging or export function can accept different destinations that provide write. Tests can replace a real service with a fake object that provides the methods used by the application. Python also relies on this idea with iterables, context managers, callable objects, and file like objects. Protocol type hints can document these expectations while keeping implementations independent.

Why Interviewers Ask This

Interviewers ask this question to check whether a candidate understands how Python decides whether different objects can be used by the same code. They want to see whether the candidate focuses on supported operations instead of only class names or inheritance. The question also tests judgment about runtime failures, clear interfaces, type hints, protocols, and abstract base classes.

Common interview mistakes

A common mistake is saying duck typing means Python has no types. Python objects still have types. The point is that code can accept different types when they support the required behavior. Another mistake is checking every object with type or isinstance even when the needed operation is enough. A matching method name is also not a complete guarantee. The method must accept compatible arguments and follow the expected meaning. Another mistake is believing Protocol changes runtime behavior. It mainly helps static type checkers unless separate runtime checking is added. Broad exception handling can also hide defects, so catch errors only where the program can respond usefully.

Interview tip

Start with the practical rule that Python uses supported behavior rather than requiring one parent class. Give one small write example. Then explain that missing methods fail at runtime and that Protocol or an abstract base class can make the expected interface clearer.

Interviewer may ask next
What happens if an object has the expected method name but an incompatible method signature?

The call can fail with TypeError when Python passes arguments that the method does not accept. Duck typing requires compatible behavior, not only a matching name. This matters because an object may appear suitable but still fail when the operation runs. Protocol definitions and static type checking can find many signature problems earlier, while tests still confirm runtime behavior.

When should you use a Protocol instead of an abstract base class?

Use a Protocol when you want to describe required methods without forcing classes to inherit from one shared parent. This keeps unrelated implementations compatible through their existing behavior. Use an abstract base class when explicit inheritance, shared implementation, or runtime membership rules are important. The tradeoff is that Protocol gives looser coupling, while an abstract base class gives stronger control over the class hierarchy.

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.