Python Interview Questions: Complete Guide for Success

May 22, 2026
Updated Jun 23, 2026
75 min read

AI Insights

Powered by GPT-4o-mini

Verified Context: python-interview-questions-complete-guide-for-success
Quick Answer

A complete original Python interview guide covering basics, data structures, functions, recursion, OOP, files, exceptions, iterators, generators, decorators, modules, output tracing, and coding practice.

Quick Summary

Prepare for your Python interview with our complete guide covering key concepts, practice problems, and expert tips for success.

Complete Python Interview Questions and Practice Problems

This guide is a complete interview revision file for the Python Foundations series.

It covers the ideas from the earlier lessons:

  • Python basics, variables, input, output, and type conversion
  • operators, control flow, loops, and pattern thinking
  • strings, lists, tuples, sets, and dictionaries
  • functions, recursion, time complexity, and memory behavior
  • OOP, encapsulation, inheritance, polymorphism, and abstraction
  • file handling, JSON, pickle, exceptions, iterators, generators, namespaces, closures, decorators, modules, imports, and packages

All questions, explanations, examples, and practice problems here are written freshly. Use this as a learning and interview-preparation guide, not as a memorization sheet.

How to Use This Interview Guide

Use three passes:

  1. Read the question and answer it out loud before looking.
  2. Compare your answer with the explanation.
  3. Run or mentally trace the code where shown.

In interviews, the best answers are usually:

  • clear
  • short at first
  • backed by one example
  • honest about edge cases
  • connected to practical use

Interview Answer Formula

For theory questions, use this structure:

  1. Define the concept.
  2. Mention why it matters.
  3. Give a small example.
  4. Mention one common mistake or edge case.

Example:

text
A list is an ordered, mutable collection in Python.
It is useful when we need to store items and update them later.
For example, scores = [10, 20, 30].
A common mistake is copying a nested list with copy(), because nested objects are still shared.

Part 1: Core Python Interview Questions

1. What is Python?

Python is a high-level, general-purpose programming language known for readable syntax and a large standard library.

It is used for web development, automation, data analysis, machine learning, scripting, testing, backend services, and many other tasks.

2. Why is Python called high-level?

Python hides many low-level details such as manual memory management and direct CPU instructions.

You write code closer to human-readable logic:

python
name = "Asha"
print(f"Welcome, {name}")

Explanation

  • The variable name is assigned the string value "Asha".
  • The print function outputs a formatted string that includes the value of name.
  • The f-string syntax (prefixing the string with f) allows for easy insertion of variable values directly within the string.
  • The output of this code will be: "Welcome, Asha".

3. Is Python interpreted or compiled?

Python is commonly called interpreted because users usually run source code directly.

In CPython, Python source code is first compiled internally into bytecode, and then that bytecode is executed by the Python virtual machine.

4. What is CPython?

CPython is the reference implementation of Python, written mainly in C.

When most people say "Python", they usually mean CPython unless they mention another implementation such as PyPy or Jython.

5. What is dynamic typing?

Dynamic typing means variable names do not have fixed declared types.

python
value = 10
value = "ten"

print(value)

Explanation

  • The variable value is initially assigned an integer of 10.
  • The variable value is then reassigned to the string "ten", overriding the previous integer value.
  • The print function outputs the current value of the variable, which will display "ten".
  • This snippet illustrates how Python allows dynamic typing, enabling variables to change types during execution.

The name value first points to an integer object and later points to a string object.

6. What is strong typing?

Strong typing means Python does not silently mix incompatible types in unsafe ways.

python
age = 25

print("Age: " + str(age))

Explanation

  • The variable age is assigned the integer value of 25.
  • The print function is used to output a string that includes the age.
  • The integer age is converted to a string using str(age) to ensure proper concatenation.
  • The final output will display "Age: 25" in the console.

Without str(age), string concatenation with an integer would fail.

7. What is PEP 8?

PEP 8 is Python's style guide.

It gives naming, formatting, indentation, spacing, and readability conventions. It is not the language grammar, but it helps teams write consistent code.

8. Why is indentation important in Python?

Indentation defines code blocks in Python.

python
score = 85

if score >= 50:
    print("pass")
else:
    print("try again")

Explanation

  • The variable score is initialized with a value of 85.
  • An if statement checks if the score is greater than or equal to 50.
  • If the condition is true, it prints "pass"; otherwise, it prints "try again".
  • This code effectively categorizes the score into a pass/fail outcome based on a threshold.

Wrong indentation can change meaning or raise an error.

9. What are keywords?

Keywords are reserved words with special meaning.

Examples include if, else, for, while, def, class, try, except, return, yield, import, global, and nonlocal.

10. What are identifiers?

Identifiers are names created by programmers for variables, functions, classes, modules, and other objects.

Good identifiers are descriptive:

python
completed_lessons = 12

Explanation

  • The variable completed_lessons is assigned the integer value of 12.
  • This could represent the total number of lessons a user has finished in an educational application.
  • The variable can be used later in the code for calculations, condition checks, or displaying progress.
  • It serves as a simple way to store and manipulate user progress data.

11. What does print() do?

print() writes text representation of objects to standard output.

python
print("Python", "Interview", sep=" - ", end="!\n")

Explanation

  • The print function is used to display text output to the console.
  • The sep parameter specifies the string inserted between multiple arguments, here it is set to " - ".
  • The end parameter defines what is printed at the end of the output; in this case, it adds an exclamation mark followed by a newline.
  • The output of this code will be: Python - Interview!

Output:

text
Python - Interview!

12. What does input() return?

input() always returns a string.

python
raw_age = input("Age: ")
age = int(raw_age)

Explanation

  • The input() function prompts the user to enter their age as a string.
  • The entered value is stored in the variable raw_age.
  • The int() function converts the string value of raw_age into an integer and assigns it to the variable age.
  • This conversion allows for numerical operations to be performed on the age value later in the program.

Convert it when you need a number.

13. What is type conversion?

Type conversion changes a value from one type to another.

python
price = "199"
quantity = 3

print(int(price) * quantity)

Explanation

  • The variable price is initialized as a string containing the value "199".
  • The variable quantity is set to an integer value of 3.
  • The int(price) function converts the string price to an integer for mathematical operations.
  • The print function outputs the result of multiplying the integer value of price by quantity, which is 597.

14. What is implicit type conversion?

Implicit conversion happens when Python safely converts one type during an expression.

python
result = 10 + 2.5

print(result)

Explanation

  • The variable result is assigned the sum of an integer (10) and a float (2.5).
  • Python automatically handles the addition of different numeric types, resulting in a float.
  • The print function outputs the value of result to the console, which will be 12.5.
  • This snippet illustrates how Python can perform arithmetic operations seamlessly.

Output:

text
12.5

15. What is explicit type conversion?

Explicit conversion is done manually with functions such as int(), float(), str(), list(), tuple(), set(), and dict().

16. What does a Python variable store?

A Python variable stores a reference to an object, not the object directly.

python
first = [1, 2]
second = first

second.append(3)

print(first)

Explanation

  • The variable first is initialized as a list containing two integers: 1 and 2.
  • The variable second is assigned to reference the same list object as first, not a copy.
  • When 3 is appended to second, it modifies the original list that both first and second reference.
  • The print(first) statement outputs the modified list, which now includes the appended value, resulting in [1, 2, 3].
  • This demonstrates how mutable objects like lists in Python can be affected by changes through multiple references.

Output:

text
[1, 2, 3]

Both names point to the same list.

17. What is object identity?

Object identity is the unique identity of an object during its lifetime.

Use id() to inspect it:

python
items = ["python"]

print(id(items))

Explanation

  • The code initializes a list named items containing a single string element "python".
  • The id() function is called with items as an argument, which returns the unique memory address of the list object.
  • The print() function outputs this memory address to the console, allowing you to see where the list is stored in memory.
  • This can be useful for understanding object identity and memory management in Python.

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

== compares values.

is compares object identity.

python
a = [1, 2]
b = [1, 2]
c = a

print(a == b)
print(a is b)
print(a is c)

Explanation

  • The code initializes two lists, a and b, with identical contents, and assigns c to reference the same list as a.
  • The expression a == b checks for value equality, returning True since both lists contain the same elements.
  • The expression a is b checks for identity, returning False because a and b are two distinct objects in memory.
  • The expression a is c checks for identity, returning True since c references the same object as a.

Output:

text
True
False
True

19. What are mutable objects?

Mutable objects can be changed after creation.

Examples include lists, dictionaries, and sets.

20. What are immutable objects?

Immutable objects cannot be changed after creation.

Examples include integers, floats, strings, tuples, booleans, and frozensets.

21. Why are strings immutable?

When you "modify" a string, Python creates a new string.

python
title = "python"
updated = title.upper()

print(title)
print(updated)

Explanation

  • The variable title is initialized with the string "python".
  • The upper() method is called on title, converting it to uppercase and storing the result in the variable updated.
  • The original string title is printed, displaying "python".
  • The uppercase version stored in updated is printed, displaying "PYTHON".

Output:

text
python
PYTHON

22. What is truthy and falsy?

Python treats some values as false in conditions.

Falsy examples:

  • False
  • None
  • 0
  • 0.0
  • ""
  • []
  • {}
  • set()

Non-empty containers and non-zero numbers are usually truthy.

23. What is None?

None represents absence of a value.

It is not the same as 0, False, or an empty string.

python
result = None

if result is None:
    print("No result yet")

Explanation

  • Initializes a variable result with a value of None.
  • Uses an if statement to check if result is still None.
  • If the condition is true, it prints "No result yet" to the console.
  • This code can be useful for debugging or handling cases where a result is expected but not yet available.

24. What is the difference between type() and isinstance()?

type() gives the exact class of an object.

isinstance() checks whether an object belongs to a class or subclass.

python
print(type(True))
print(isinstance(True, int))

Explanation

  • The print(type(True)) statement outputs the type of the Boolean value True, which is <class 'bool'>.
  • The print(isinstance(True, int)) statement checks if True is an instance of the int class, returning True since in Python, True is treated as 1.
  • This code demonstrates the dual nature of Boolean values in Python, where they are subclasses of integers.
  • It highlights how Boolean values can be used interchangeably with integers in certain contexts, such as arithmetic operations.

Output:

text
<class 'bool'>
True

25. What is floor division?

Floor division uses // and returns the floor of the division result.

python
print(7 // 2)
print(-7 // 2)

Explanation

  • The // operator performs floor division, which returns the largest integer less than or equal to the division result.
  • The first line print(7 // 2) calculates the floor division of 7 by 2, resulting in 3.
  • The second line print(-7 // 2) calculates the floor division of -7 by 2, resulting in -4, demonstrating how floor division rounds down towards negative infinity.
  • This behavior is important for understanding how Python handles division with negative numbers compared to other programming languages.

Output:

text
3
-4

26. What is the modulo operator?

% returns the remainder after division.

python
print(17 % 5)

Explanation

  • The print function outputs the result of the expression inside it to the console.
  • The expression 17 % 5 uses the modulus operator %, which returns the remainder of the division of 17 by 5.
  • In this case, 17 divided by 5 equals 3 with a remainder of 2, so the output will be 2.
  • This operation is useful for determining if a number is even or odd, or for cyclic calculations.

Output:

text
2

27. What is operator precedence?

Operator precedence decides which part of an expression is evaluated first.

python
print(2 + 3 * 4)
print((2 + 3) * 4)

Explanation

  • The first print statement calculates 2 + 3 * 4, where multiplication has higher precedence than addition, resulting in 2 + 12, which equals 14.
  • The second print statement calculates (2 + 3) * 4, where the parentheses force the addition to be performed first, resulting in 5 * 4, which equals 20.
  • This snippet demonstrates how operator precedence and parentheses can affect the outcome of arithmetic operations in Python.
  • It highlights the importance of understanding the order of operations to avoid unexpected results in calculations.

Output:

text
14
20

28. What is short-circuit evaluation?

Python may stop evaluating a logical expression once the result is already known.

python
name = ""

if name and name[0] == "A":
    print("Starts with A")
else:
    print("No usable name")

Explanation

  • Initializes an empty string variable name.
  • Uses a conditional statement to check if name is non-empty and if its first character is 'A'.
  • If both conditions are true, it prints "Starts with A".
  • If either condition is false, it prints "No usable name".

Because name is empty, name[0] is not evaluated.

29. What is a conditional expression?

It is Python's compact if-else expression.

python
score = 72
status = "pass" if score >= 50 else "fail"

print(status)

Explanation

  • A variable score is initialized with a value of 72.
  • A conditional expression assigns "pass" to the variable status if score is 50 or higher; otherwise, it assigns "fail".
  • The print function outputs the value of status, which will be "pass" in this case.

30. What is the difference between for and while loops?

Use for when you are iterating over a known iterable.

Use while when the loop depends on a condition that may change.

31. What are break, continue, and pass?

break exits the loop.

continue skips to the next iteration.

pass does nothing and is used as a placeholder.

32. What does range() produce?

range() produces an immutable sequence-like object that generates numbers lazily.

python
for number in range(2, 7, 2):
    print(number)

Explanation

  • The range(2, 7, 2) function generates a sequence of numbers starting from 2 up to, but not including, 7, with a step of 2.
  • The loop iterates through the generated numbers, which are 2, 4, and 6.
  • The print(number) statement outputs each number in the sequence to the console.
  • This code effectively showcases how to work with ranges and control the increment in a loop.

Output:

text
2
4
6

33. What is loop else?

A loop else block runs when the loop finishes normally, without break.

python
target = 7
numbers = [2, 4, 6]

for number in numbers:
    if number == target:
        print("found")
        break
else:
    print("not found")

Explanation

  • The variable target is set to 7, which is the number the code will search for in the list numbers.
  • The list numbers contains three integers: 2, 4, and 6.
  • A for loop iterates through each number in the numbers list.
  • If a number matches the target, it prints "found" and exits the loop using break.
  • If the loop completes without finding the target, the else block executes, printing "not found".

34. What are membership operators?

in and not in test membership.

python
tags = ["python", "oop", "files"]

print("oop" in tags)

Explanation

  • A list named tags is created containing three string elements: "python", "oop", and "files".
  • The print function is used to output the result of the expression "oop" in tags.
  • The expression evaluates to True if "oop" is found in the tags list, and False otherwise.
  • This demonstrates the use of the in keyword for membership testing in Python lists.

35. What are bitwise operators?

Bitwise operators work on integer bits.

Common examples are &, |, ^, ~, <<, and >>.

They are useful in flags, masks, permissions, and low-level tasks.

36. How does string indexing work?

Strings are indexed from 0.

Negative indexes count from the end.

python
word = "python"

print(word[0])
print(word[-1])

Explanation

  • The variable word is assigned the string value "python".
  • print(word[0]) outputs the first character of the string, which is 'p'.
  • print(word[-1]) outputs the last character of the string, which is 'n'.
  • This code demonstrates how to use positive and negative indexing to access string elements.

Output:

text
p
n

37. How does slicing work?

Slicing uses start:stop:step.

python
word = "interview"

print(word[0:5])
print(word[::-1])

Explanation

  • The variable word is initialized with the string "interview".
  • The first print statement outputs the first five characters of the string, resulting in "inter".
  • The second print statement uses slicing with a step of -1 to reverse the string, producing "weivretni".

Output:

text
inter
weivretni

38. Why should you prefer join() for combining many strings?

Repeated string concatenation can create many intermediate strings.

join() is clearer and usually more efficient for many pieces.

python
parts = ["python", "is", "clear"]

sentence = " ".join(parts)
print(sentence)

Explanation

  • A list named parts is created containing three string elements: "python", "is", and "clear".
  • The join() method is used on a space character to concatenate the elements of the list into a single string, with spaces in between.
  • The resulting string is stored in the variable sentence.
  • Finally, the print() function outputs the concatenated sentence to the console.

39. What is the difference between find() and index() on strings?

find() returns -1 if the substring is missing.

index() raises ValueError if the substring is missing.

40. What do split() and strip() do?

strip() removes surrounding whitespace or selected characters.

split() breaks a string into a list.

python
line = "  python,oop,files  "

print(line.strip().split(","))

Explanation

  • The line variable contains a string with leading and trailing spaces and comma-separated values.
  • The strip() method removes any whitespace from the beginning and end of the string.
  • The split(",") method then divides the cleaned string into a list using the comma as a delimiter.
  • The final output is a list of strings: ['python', 'oop', 'files'], with no extra spaces.

41. What is a list?

A list is an ordered, mutable collection.

python
scores = [90, 75, 88]
scores.append(92)

print(scores)

Explanation

  • A list named scores is initialized with three integer values representing scores.
  • The append method is used to add a new score, 92, to the end of the scores list.
  • The print function outputs the updated list, which now contains four scores: [90, 75, 88, 92].

42. What is the difference between append() and extend()?

append() adds one object as a single item.

extend() adds each item from an iterable.

python
items = [1, 2]
items.append([3, 4])
print(items)

items = [1, 2]
items.extend([3, 4])
print(items)

Explanation

  • The append method adds its argument as a single element to the end of the list, resulting in a nested list when a list is appended.
  • In the first case, items.append([3, 4]) results in items being [1, 2, [3, 4]].
  • The extend method, on the other hand, iterates over its argument and adds each element to the list, effectively flattening the input.
  • In the second case, items.extend([3, 4]) results in items being [1, 2, 3, 4].
  • This distinction is crucial when manipulating lists in Python, as it affects the structure of the resulting list.

43. What is list aliasing?

Aliasing happens when two names refer to the same list.

python
primary = ["draft"]
alias = primary

alias.append("review")

print(primary)

Explanation

  • The variable primary is initialized as a list containing a single string element "draft".
  • The variable alias is assigned to reference the same list object as primary, meaning both variables point to the same memory location.
  • When "review" is appended to alias, it modifies the list that both primary and alias reference.
  • The print(primary) statement outputs the modified list, which now contains both "draft" and "review".

44. How do you copy a list?

For a shallow copy, use copy(), slicing, or list().

python
original = ["a", "b"]
clone = original.copy()

Explanation

  • The variable original is initialized as a list containing two string elements: "a" and "b".
  • The copy() method is called on the original list to create a new list, clone, which contains the same elements.
  • The clone list is a shallow copy, meaning changes to clone will not affect original, and vice versa.
  • This method is useful for preserving the original list while allowing modifications to the copy.

For nested mutable objects, use copy.deepcopy() when you need independent nested data.

45. What is a tuple?

A tuple is an ordered, immutable collection.

It is useful for fixed records, multiple return values, and dictionary keys when all items are hashable.

46. Why does a one-item tuple need a comma?

The comma creates the tuple, not the parentheses.

python
not_tuple = (10)
one_item_tuple = (10,)

print(type(not_tuple))
print(type(one_item_tuple))

Explanation

  • The variable not_tuple is assigned a single integer value, which does not create a tuple.
  • The variable one_item_tuple is correctly defined as a tuple containing one item by including a trailing comma.
  • The type() function is used to check the data types of both variables.
  • The output will show that not_tuple is of type int, while one_item_tuple is of type tuple.
  • This snippet highlights the importance of the comma in tuple creation when defining single-item tuples in Python.

47. What is tuple unpacking?

Tuple unpacking assigns values from a tuple-like iterable to names.

python
name, score = ("Neha", 91)

print(name)
print(score)

Explanation

  • A tuple containing a name and a score is created and assigned to the variables name and score simultaneously.
  • The first element of the tuple, "Neha", is assigned to the variable name.
  • The second element, 91, is assigned to the variable score.
  • The print statements output the values of name and score to the console.

48. What is a set?

A set is an unordered collection of unique hashable items.

python
topics = {"python", "oop", "python"}

print(topics)

Explanation

  • A set named topics is initialized with three elements: "python", "oop", and "python".
  • Sets in Python automatically eliminate duplicate entries, so "python" appears only once in the final set.
  • The print function outputs the contents of the topics set to the console.
  • The order of elements in a set is not guaranteed, as sets are unordered collections.

49. Why are lists not allowed inside sets?

Set elements must be hashable.

Lists are mutable and unhashable, so they cannot be set elements.

50. What is a dictionary?

A dictionary maps keys to values.

Keys must be hashable. Values can be almost any object.

python
profile = {"name": "Ira", "score": 88}

print(profile["name"])

Explanation

  • A dictionary named profile is created with two key-value pairs: "name" and "score".
  • The value associated with the key "name" is accessed using profile["name"].
  • The print function outputs the value of "name", which is "Ira", to the console.

51. Why does dictionary membership check keys?

in checks dictionary keys because keys are the lookup mechanism.

python
profile = {"name": "Ira", "score": 88}

print("name" in profile)
print("Ira" in profile)

Explanation

  • A dictionary named profile is created with two key-value pairs: "name" and "score".
  • The first print statement checks if the key "name" exists in the profile dictionary, returning True.
  • The second print statement checks if the value "Ira" exists in the profile dictionary, returning False since it is not a key.

52. What are dictionary views?

keys(), values(), and items() return dynamic view objects.

They reflect dictionary changes.

53. When should you use a list, tuple, set, or dictionary?

Use a list for ordered mutable sequences.

Use a tuple for fixed records.

Use a set for uniqueness and membership tests.

Use a dictionary for key-value lookup.

54. What is a function?

A function is a reusable block of code that can receive inputs and return output.

python
def add_bonus(score, bonus):
    return score + bonus

Explanation

  • The function add_bonus takes two parameters: score and bonus.
  • It returns the sum of score and bonus, effectively increasing the score by the specified bonus amount.
  • This function can be used in scoring systems where additional points are awarded, such as in games or assessments.
  • It is a simple and reusable function that promotes code clarity and modularity.

55. What is the difference between parameter and argument?

A parameter is the name in the function definition.

An argument is the value passed during the function call.

56. What is the difference between print and return?

print displays a value.

return sends a value back to the caller.

python
def double(number):
    return number * 2

result = double(5)
print(result)

Explanation

  • The double function takes a single parameter, number.
  • It returns the value of number multiplied by 2.
  • The function is called with the argument 5, and the result is stored in the variable result.
  • Finally, the value of result is printed, which outputs 10.

57. What does a function return if there is no return statement?

It returns None.

python
def greet():
    print("hello")

value = greet()
print(value)

Explanation

  • The greet function is defined to print the string "hello" when called.
  • The function is invoked with value = greet(), which executes the print statement inside the function.
  • The return value of the greet function is None since it does not explicitly return anything, which is stored in the variable value.
  • The final print statement outputs the value of value, which will be None.

58. What are default arguments?

Default arguments are used when the caller does not provide a value.

python
def create_title(text, prefix="Lesson"):
    return f"{prefix}: {text}"

Explanation

  • The function create_title takes two parameters: text (the main title) and prefix (defaulting to "Lesson").
  • It uses an f-string to concatenate the prefix and the text, ensuring a consistent format.
  • The return value is a string that combines the prefix and the text, separated by a colon.
  • This function can be useful for creating standardized titles in educational or instructional content.

59. Why are mutable default arguments risky?

The default object is created once when the function is defined, not each time it is called.

Safer version:

python
def add_tag(tag, tags=None):
    if tags is None:
        tags = []

    tags.append(tag)
    return tags

Explanation

  • The function add_tag takes two parameters: tag (the tag to be added) and tags (an optional list of existing tags).
  • If tags is not provided (i.e., it is None), a new empty list is created to hold the tags.
  • The specified tag is appended to the tags list.
  • The updated list of tags is returned, allowing for easy accumulation of tags over multiple function calls.
  • This approach ensures that each call to add_tag can either modify an existing list or create a new one if none is provided.

60. What are *args and **kwargs?

*args collects extra positional arguments.

**kwargs collects extra keyword arguments.

python
def describe_event(*labels, **details):
    return labels, details

print(describe_event("python", "live", speaker="Maya"))

Explanation

  • The function describe_event accepts a variable number of positional arguments (*labels) and keyword arguments (**details).
  • Positional arguments are collected into a tuple named labels, while keyword arguments are stored in a dictionary named details.
  • The function returns both labels and details, allowing for flexible input regarding event characteristics.
  • In the provided print statement, the function is called with two positional arguments ("python", "live") and one keyword argument (speaker="Maya").

61. What is a lambda function?

A lambda is a small anonymous function expression.

python
scores = [3, 10, 2]
print(sorted(scores, key=lambda value: value % 3))

Explanation

  • The list scores contains three integer values: 3, 10, and 2.
  • The sorted() function is used to sort the list, with a custom sorting key defined by a lambda function.
  • The lambda function calculates the remainder of each score when divided by 3 (value % 3).
  • The sorting will arrange the scores in ascending order based on these remainders.
  • The output will be a new list of scores sorted according to their remainders: [3, 10, 2] becomes [3, 2, 10].

Use normal def when logic needs a name, documentation, or multiple statements.

62. What are first-class functions?

Functions are first-class objects in Python.

You can assign them to variables, pass them as arguments, return them from functions, and store them in containers.

63. What is scope?

Scope decides where a name can be accessed.

Python follows LEGB lookup:

  • Local
  • Enclosing
  • Global
  • Built-in

64. What is the global keyword?

global tells Python that assignment should target a module-level name.

Use it carefully because it can make code harder to reason about.

65. What is the nonlocal keyword?

nonlocal lets a nested function assign to a name from an enclosing function scope.

python
def make_counter():
    count = 0

    def next_count():
        nonlocal count
        count += 1
        return count

    return next_count

Explanation

  • The make_counter function initializes a local variable count to zero.
  • It defines an inner function next_count that increments the count variable and returns its updated value.
  • The nonlocal keyword allows the inner function to modify the count variable from the enclosing scope.
  • The make_counter function returns the next_count function, enabling the creation of multiple independent counter instances.
  • Each call to the returned function will increase the count, demonstrating the concept of closures in Python.

66. What is a closure?

A closure is a function that remembers values from a surrounding function even after that surrounding function has finished.

python
def make_multiplier(factor):
    def multiply(number):
        return number * factor

    return multiply

triple = make_multiplier(3)
print(triple(4))

Explanation

  • The make_multiplier function takes a single argument factor and defines an inner function multiply.
  • The multiply function takes a number and returns the product of number and factor.
  • make_multiplier returns the multiply function, effectively creating a closure that retains the factor value.
  • The variable triple is assigned the result of make_multiplier(3), creating a function that triples its input.
  • The print(triple(4)) statement outputs 12, which is the result of multiplying 4 by 3.

67. What is a decorator?

A decorator wraps a function to add behavior before, after, or around the original function.

python
import functools

def announce(function_to_call):
    @functools.wraps(function_to_call)
    def wrapper(*args, **kwargs):
        print("starting")
        return function_to_call(*args, **kwargs)

    return wrapper

Explanation

  • The announce function is a decorator that takes another function as an argument.
  • It uses functools.wraps to preserve the metadata of the original function being wrapped.
  • Inside the wrapper function, it prints "starting" before calling the original function with any provided arguments.
  • The wrapper function is returned, effectively replacing the original function with the new behavior when the decorator is applied.

68. Why use functools.wraps in decorators?

It preserves the wrapped function's name, docstring, annotations, and other metadata.

This helps debugging, testing, documentation, and frameworks.

69. What is recursion?

Recursion is when a function calls itself to solve a smaller version of the same problem.

Every recursive solution needs:

  • a base case
  • progress toward the base case

70. What is a base case?

A base case is the stopping condition in recursion.

python
def factorial(number):
    if number == 0:
        return 1

    return number * factorial(number - 1)

Explanation

  • The function factorial takes a single argument number.
  • It checks if number is 0; if true, it returns 1, as the factorial of 0 is defined as 1.
  • If number is greater than 0, it recursively calls itself with number - 1 and multiplies the result by number.
  • This process continues until it reaches the base case of 0, effectively calculating the factorial through repeated multiplication.
  • The function demonstrates the concept of recursion, where a function calls itself to solve smaller instances of the same problem.

71. What is memoization?

Memoization stores results of expensive function calls so repeated calls can reuse the answer.

python
def fibonacci(number, cache=None):
    if cache is None:
        cache = {}

    if number in cache:
        return cache[number]

    if number <= 1:
        return number

    cache[number] = fibonacci(number - 1, cache) + fibonacci(number - 2, cache)
    return cache[number]

Explanation

  • Defines a recursive function fibonacci that computes the Fibonacci number for a given input number.
  • Utilizes a cache dictionary to store previously computed Fibonacci numbers, enhancing performance by avoiding redundant calculations.
  • Checks if the cache is None and initializes it to an empty dictionary if so, ensuring that memoization works correctly.
  • Returns the Fibonacci number directly from the cache if it has already been computed, significantly reducing the time complexity.
  • Handles base cases where the input number is 0 or 1, returning the number itself in those scenarios.

72. What is Big O notation?

Big O describes how runtime or memory grows as input size grows.

It ignores machine-specific details and focuses on growth pattern.

73. What are common time complexities?

Common examples:

ComplexityMeaning
O(1)constant time
O(log n)logarithmic time
O(n)linear time
O(n log n)common efficient sorting time
O(n^2)nested loop over same input

74. What is space complexity?

Space complexity describes how extra memory grows with input size.

A function that creates a new list of size n usually has O(n) extra space.

75. What is aliasing?

Aliasing means multiple names point to the same object.

This matters most with mutable objects.

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

A shallow copy creates a new outer container but keeps references to nested objects.

A deep copy recursively copies nested objects.

python
import copy

matrix = [[1], [2]]
shallow = matrix.copy()
deep = copy.deepcopy(matrix)

matrix[0].append(99)

print(shallow)
print(deep)

Explanation

  • The code imports the copy module to utilize its copying functionalities.
  • A 2D list matrix is created with two sublists containing integers.
  • A shallow copy of matrix is made using the copy() method, which copies the outer list but references the inner lists.
  • A deep copy of matrix is created using copy.deepcopy(), which creates a new list and recursively copies all inner lists.
  • Modifying the first sublist of matrix by appending 99 affects the shallow copy but not the deep copy, demonstrating the difference in how they handle nested objects.

77. What is garbage collection?

Garbage collection frees memory used by objects that are no longer reachable.

CPython mainly uses reference counting and also has a cyclic garbage collector.

78. What is hashability?

An object is hashable if it has a stable hash value during its lifetime and can be compared for equality.

Hashable objects can be dictionary keys and set elements.

79. What is a class?

A class is a blueprint for creating objects with related data and behavior.

python
class Lesson:
    def __init__(self, title):
        self.title = title

Explanation

  • The Lesson class is defined to encapsulate the concept of a lesson.
  • The __init__ method is a constructor that initializes a new instance of the class.
  • The constructor takes one parameter, title, which is assigned to the instance variable self.title.
  • This allows each lesson object to have a unique title when created.

80. What is an object?

An object is an instance of a class.

python
lesson = Lesson("Decorators")
print(lesson.title)

Explanation

  • A Lesson object is instantiated with the title "Decorators".
  • The title attribute of the lesson object is accessed and printed to the console.
  • This showcases basic object-oriented programming principles in Python, specifically attribute access.
  • The output will display the string "Decorators" when the code is executed.

81. What is self?

self is the conventional name for the current object inside instance methods.

Python passes it automatically when you call a method on an object.

82. What is __init__?

__init__ initializes a newly created object.

It is not a constructor in the exact low-level sense, but beginners can think of it as setup logic for an object.

83. What is encapsulation?

Encapsulation means keeping data and behavior together and controlling access through methods or properties.

It helps protect object state.

84. What is name mangling?

Names that start with two underscores inside a class are rewritten by Python to include the class name.

This reduces accidental name collisions in subclasses.

85. What is a property?

property lets method-like logic be accessed like an attribute.

python
class Progress:
    def __init__(self, completed, total):
        self.completed = completed
        self.total = total

    @property
    def percent(self):
        return round(self.completed / self.total * 100)

Explanation

  • The Progress class is initialized with two parameters: completed and total, representing the number of completed tasks and the total tasks, respectively.
  • The percent property calculates the completion percentage by dividing completed by total, multiplying by 100, and rounding the result to the nearest whole number.
  • The use of the @property decorator allows percent to be accessed like an attribute, providing a clean interface for retrieving the completion percentage without needing to call it as a method.
  • This class can be useful for tracking progress in various applications, such as project management or task completion systems.

86. What is a class variable?

A class variable belongs to the class and is shared through instances unless shadowed by an instance attribute.

87. What is an instance variable?

An instance variable belongs to a specific object.

It is usually created with self.name = value.

88. What is a static method?

A static method is a function placed inside a class namespace that does not receive self or cls.

Use it for helper behavior closely related to the class.

89. What is a class method?

A class method receives the class as cls.

It is useful for alternate constructors and class-level behavior.

90. What is inheritance?

Inheritance lets one class reuse or extend behavior from another class.

The child class is a specialized form of the parent class.

91. What does super() do?

super() gives access to parent-class behavior according to Python's method resolution order.

It is commonly used to call the parent __init__.

92. What is method overriding?

Method overriding happens when a child class defines a method with the same name as a parent method.

The child version is used for child objects.

93. What is polymorphism?

Polymorphism means different objects can respond to the same method call in their own way.

python
class TextLesson:
    def render(self):
        return "text"

class VideoLesson:
    def render(self):
        return "video"

for item in [TextLesson(), VideoLesson()]:
    print(item.render())

Explanation

  • Defines two classes, TextLesson and VideoLesson, each with a render method that returns a string indicating the type of lesson.
  • The for loop iterates over a list containing instances of both classes.
  • Each lesson's render method is called, showcasing how different classes can be treated uniformly through their shared interface.
  • The output will be "text" followed by "video", illustrating the concept of polymorphism in object-oriented programming.

94. What is abstraction?

Abstraction means exposing essential behavior while hiding implementation details.

Abstract base classes can define required methods for subclasses.

95. What is composition?

Composition means building one object using other objects.

Use composition when an object "has a" helper object.

Inheritance is better when an object truly "is a" specialized version of another object.

96. What are special methods?

Special methods are double-underscore methods such as __str__, __len__, __add__, and __iter__.

They let your objects work naturally with Python syntax and built-ins.

97. What is file handling?

File handling means reading from and writing to files.

Use a context manager so files close automatically:

python
from pathlib import Path

path = Path("notes.txt")
text = path.read_text(encoding="utf-8")

Explanation

  • Imports the Path class from the pathlib module, which provides an object-oriented approach to handling filesystem paths.
  • Creates a Path object for the file named "notes.txt".
  • Uses the read_text method of the Path object to read the entire contents of the file as a string, specifying UTF-8 encoding for proper text handling.
  • The resulting string is stored in the variable text, which can be used for further processing or analysis.

98. What are common file modes?

Common modes:

ModeMeaning
rread text
wwrite text and replace existing content
aappend text
rbread binary
wbwrite binary

99. What is serialization?

Serialization converts an object into a format that can be stored or transmitted.

JSON serialization is common for simple structured data.

100. What is the difference between JSON and pickle?

JSON is text-based, language-independent, and safer for data exchange.

Pickle is Python-specific and can preserve more Python objects, but loading untrusted pickle data is unsafe.

101. What is an exception?

An exception is an error event that interrupts normal program flow.

You can handle expected errors with try and except.

102. Why should you catch specific exceptions?

Catching specific exceptions avoids hiding unrelated bugs.

python
try:
    score = int("92")
except ValueError:
    score = 0

Explanation

  • The code attempts to convert a string "92" into an integer and assigns it to the variable score.
  • If the conversion fails due to a ValueError, it catches the exception and assigns 0 to score.
  • This ensures that the program can handle invalid inputs gracefully without crashing.
  • The use of try and except blocks is a common practice for managing exceptions in Python.

103. What do else and finally do in exception handling?

else runs only if the try block succeeds.

finally runs whether an exception happened or not.

104. How do you raise a custom exception?

Create a class that inherits from Exception.

python
class InvalidProgressError(Exception):
    pass

def set_progress(value):
    if not 0 <= value <= 100:
        raise InvalidProgressError("progress must be between 0 and 100")

Explanation

  • Defines a custom exception class InvalidProgressError that inherits from the built-in Exception class.
  • The function set_progress takes a parameter value and checks if it is within the range of 0 to 100.
  • If the value is outside this range, it raises the InvalidProgressError with a descriptive message.
  • This implementation helps in enforcing constraints on progress values, ensuring they remain valid.

105. What is an iterable?

An iterable is an object that can return an iterator using iter().

Lists, tuples, strings, dictionaries, sets, ranges, and files are common iterables.

106. What is an iterator?

An iterator is an object with __iter__() and __next__().

It remembers where it is during iteration.

107. What is a generator?

A generator is a function that uses yield to produce values lazily.

python
def count_up_to(limit):
    number = 1

    while number <= limit:
        yield number
        number += 1

Explanation

  • The function count_up_to takes a single parameter limit, which defines the maximum number to generate.
  • It initializes a variable number to 1, which serves as the starting point for counting.
  • A while loop is used to iterate as long as number is less than or equal to limit.
  • The yield statement allows the function to return the current value of number and pause its state, enabling it to resume on the next call.
  • This approach is memory efficient, as it generates numbers on-the-fly rather than storing them in a list.

108. What is the difference between yield and return?

yield pauses a generator and sends one value.

return ends a normal function, or ends a generator if used inside one.

109. What is lazy evaluation?

Lazy evaluation means values are produced only when needed.

Generators are useful when data is large or infinite.

110. What is a module?

A module is a Python file that can contain variables, functions, classes, and executable code.

It can be imported by another Python file.

111. What is the purpose of if __name__ == "__main__"?

It lets a file run certain code only when executed directly, not when imported.

python
def main():
    print("Run the program")

if __name__ == "__main__":  # direct run only
    main()

Explanation

  • The main function contains a single print statement that outputs "Run the program" to the console.
  • The if __name__ == "__main__": condition checks if the script is being executed as the main program, ensuring that main() is called only in that context.
  • This structure allows the script to be imported as a module in other scripts without executing the main function automatically.
  • It promotes better organization and modularity in Python code by separating the execution logic from the function definitions.

112. What happens when you import a module?

Python finds the module, runs its top-level code once, creates a module object, and stores it in the import cache.

Later imports usually reuse the cached module.

113. What are different import styles?

Common styles:

python
import math
import statistics as stats
from pathlib import Path

Explanation

  • The math module provides access to mathematical functions like trigonometry, logarithms, and constants.
  • The statistics module, imported as stats, offers functions for statistical calculations such as mean, median, and standard deviation.
  • The Path class from the pathlib module is used for object-oriented file system paths, making file manipulation easier and more intuitive.
  • This code sets up the environment for performing complex calculations and managing file paths effectively in a Python program.

Prefer clear imports and avoid from module import * in normal application code.

114. What is a package?

A package is a directory of related modules.

Modern Python supports namespace packages, and regular packages commonly contain an __init__.py file.

115. What is PyPI?

PyPI is the Python Package Index, a public repository of third-party Python packages.

Tools such as pip can install packages from it.

116. What is sys.path?

sys.path is the list of locations Python searches when importing modules.

It includes the script directory, environment-specific paths, installed package locations, and paths added by the user or tools.

117. What is dir() useful for?

dir() lists names available on an object or in a namespace.

It is useful for exploration, but documentation is better for serious learning.

118. What is a docstring?

A docstring is a string placed at the start of a module, class, or function to document it.

python
def calculate_average(values):
    """Return the arithmetic mean of a non-empty list."""
    return sum(values) / len(values)

Explanation

  • The function calculate_average takes a single parameter values, which is expected to be a non-empty list of numbers.
  • It uses the built-in sum() function to calculate the total of all elements in the list.
  • The total is then divided by the number of elements in the list, obtained using len(), to compute the average.
  • The function returns the calculated average as a float.
  • It assumes that the input list is non-empty, so no error handling for empty lists is included.

119. What is the Zen of Python?

The Zen of Python is a set of design principles for Python.

You can view it with:

python
import this

Explanation

  • The import this statement imports a module that displays the Zen of Python.
  • The Zen of Python is a collection of aphorisms that capture the philosophy of Python programming.
  • When executed, it prints a list of guiding principles that emphasize readability, simplicity, and explicitness.
  • This module is often used to inspire Python developers and remind them of best practices.

In interviews, it is enough to mention readability, simplicity, explicitness, and practical design.

120. What makes a good Python interview answer?

A good answer explains behavior, not only syntax.

For coding questions, mention:

  • input assumptions
  • edge cases
  • time complexity
  • space complexity
  • why the chosen data structure fits

Part 2: Output Prediction Questions

Use these to practice tracing Python behavior.

Output 1: Aliasing

python
first = [1, 2]
second = first
third = first.copy()

second.append(3)
third.append(4)

print(first)
print(third)

Explanation

  • The variable first is initialized as a list containing the elements 1 and 2.
  • The variable second is assigned to reference the same list as first, meaning changes to second will affect first.
  • The variable third is created as a copy of first, so it is a separate list that initially contains the same elements.
  • When 3 is appended to second, it modifies the original list first, resulting in first being [1, 2, 3].
  • Appending 4 to third does not affect first, so third remains [1, 2, 4], demonstrating the difference between referencing and copying lists.

Answer:

text
[1, 2, 3]
[1, 2, 4]

Output 2: Truthy and Falsy

python
values = [0, "", [], "python"]

for value in values:
    print(bool(value))

Explanation

  • A list named values is created containing different data types: an integer, a string, an empty list, and a non-empty string.
  • The for loop iterates over each item in the values list.
  • The bool() function is called on each value, converting it to its boolean equivalent based on Python's truthiness rules.
  • The results of the boolean evaluations are printed to the console, showing False for falsy values (0, "", []) and True for truthy values ("python").
  • This snippet illustrates how Python treats various data types in conditional contexts.

Answer:

text
False
False
False
True

Output 3: Loop Else

python
numbers = [2, 4, 6]

for number in numbers:
    if number % 2 == 1:
        print("odd found")
        break
else:
    print("all even")

Explanation

  • A list named numbers is initialized with three even integers: 2, 4, and 6.
  • A for loop iterates through each number in the numbers list.
  • Inside the loop, an if statement checks if the current number is odd by using the modulus operator (%).
  • If an odd number is found, it prints "odd found" and exits the loop using break.
  • If the loop completes without finding any odd numbers, the else block executes, printing "all even".

Answer:

text
all even

Output 4: Default Argument Trap

python
def add_item(item, bucket=[]):
    bucket.append(item)
    return bucket

print(add_item("a"))
print(add_item("b"))

Explanation

  • The function add_item takes an item and an optional bucket list, which defaults to an empty list if not provided.
  • When an item is added to bucket, it modifies the same list object across multiple function calls due to the mutable nature of lists.
  • The first call add_item("a") appends "a" to the default list, while the second call add_item("b") appends "b" to the same list, resulting in ["a", "b"].
  • This behavior can lead to unexpected results if the function is called multiple times, as the default list retains its state between calls.
  • To avoid this issue, it's recommended to use None as a default value and initialize the list inside the function.

Answer:

text
['a']
['a', 'b']

The same default list is reused.

Output 5: Dictionary Key Replacement

python
data = {"a": 1, "b": 2, "a": 3}

print(data)

Explanation

  • A dictionary in Python is a collection of key-value pairs where each key must be unique.
  • In the provided code, the key "a" is defined twice with different values (1 and 3).
  • When the dictionary is created, the second definition of "a" (value 3) overwrites the first (value 1).
  • The output of print(data) will show {'a': 3, 'b': 2}, reflecting the final state of the dictionary.
  • This behavior highlights the importance of unique keys in dictionaries to avoid unintentional data loss.

Answer:

text
{'a': 3, 'b': 2}

The later value replaces the earlier value for the same key.

Output 6: Set Uniqueness

python
items = {1, True, 2, False, 0}

print(len(items))

Explanation

  • A set named items is created containing five elements: integers and boolean values.
  • Sets in Python automatically remove duplicate values, so True and 1 are considered the same, as well as False and 0.
  • The len() function is used to calculate the number of unique elements in the set.
  • The output of print(len(items)) will be 3, reflecting the unique values: {1, 2, False}.

Answer:

text
3

1 and True compare equal. 0 and False compare equal.

Output 7: Closure

python
def make_adder(amount):
    def add(number):
        return number + amount

    return add

add_five = make_adder(5)
print(add_five(10))

Explanation

  • The make_adder function takes a parameter amount and defines an inner function add that adds this amount to a given number.
  • The inner function add captures the amount variable from its enclosing scope, creating a closure.
  • When make_adder(5) is called, it returns a new function add_five that adds 5 to its input.
  • The print(add_five(10)) statement outputs 15, as it adds 5 to the input value of 10.

Answer:

text
15

Output 8: Generator State

python
def numbers():
    yield 1
    yield 2
    yield 3

stream = numbers()

print(next(stream))
print(next(stream))

Explanation

  • The numbers function is defined as a generator using the yield keyword, which allows it to produce a sequence of values one at a time.
  • When numbers() is called, it returns a generator object that can be iterated over.
  • The next() function is used to retrieve the next value from the generator, which in this case will output 1 on the first call and 2 on the second call.
  • The generator maintains its state between calls, allowing it to continue yielding values from where it left off.

Answer:

text
1
2

Output 9: Inheritance

python
class Base:
    def message(self):
        return "base"

class Child(Base):
    def message(self):
        return "child"

item = Child()
print(item.message())

Explanation

  • Defines a base class Base with a method message that returns the string "base".
  • Creates a subclass Child that overrides the message method to return the string "child".
  • Instantiates an object item of the Child class.
  • Calls the message method on the item object, which executes the overridden method in the Child class, printing "child".

Answer:

text
child

Output 10: Exception Flow

python
try:
    number = int("10")
except ValueError:
    print("bad")
else:
    print("good")
finally:
    print("done")

Explanation

  • The try block attempts to convert the string "10" into an integer.
  • If a ValueError occurs during the conversion, the except block executes, printing "bad".
  • If the conversion is successful, the else block runs, printing "good".
  • The finally block executes regardless of whether an exception occurred, printing "done".
  • This structure ensures that cleanup or final actions are performed after the try-except logic.

Answer:

text
good
done

Output 11: List Comprehension Scope

python
numbers = [1, 2, 3]
squares = [number * number for number in numbers]

print(squares)

Explanation

  • A list called numbers is initialized with three integer values: 1, 2, and 3.
  • A list comprehension is used to generate a new list called squares, where each element is the square of the corresponding element in numbers.
  • The expression number * number calculates the square of each number during the iteration.
  • Finally, the print function outputs the squares list, which contains the squared values [1, 4, 9].

Answer:

text
[1, 4, 9]

Output 12: Slicing

python
text = "abcdef"

print(text[1:5:2])
print(text[::-1])

Explanation

  • The first print statement print(text[1:5:2]) slices the string text from index 1 to 5, taking every 2nd character, resulting in "bd".
  • The second print statement print(text[::-1]) reverses the string text by using a step of -1, producing "fedcba".
  • String slicing allows for flexible extraction of substrings and manipulation of string data in Python.
  • The syntax text[start:end:step] is a powerful feature for accessing specific parts of a string efficiently.

Answer:

text
bd
fedcba

Output 13: Class Variable

python
class Counter:
    total = 0

    def __init__(self):
        Counter.total += 1

Counter()
Counter()

print(Counter.total)

Explanation

  • The Counter class has a class variable total initialized to 0, which keeps track of the number of instances.
  • The __init__ method increments the total variable by 1 each time a new instance of Counter is created.
  • Two instances of Counter are created, triggering the __init__ method twice, thus increasing the total to 2.
  • Finally, the current value of Counter.total is printed, which outputs the total number of instances created.

Answer:

text
2

Output 14: Function Name Preservation

python
import functools

def keep_name(function_to_call):
    @functools.wraps(function_to_call)
    def wrapper():
        return function_to_call()

    return wrapper

@keep_name
def load_page():
    return "ok"

print(load_page.__name__)

Explanation

  • The keep_name function is a decorator that takes another function as an argument and wraps it in a new function called wrapper.
  • The functools.wraps decorator is used to ensure that the metadata of the original function (like its name) is preserved in the wrapper function.
  • The load_page function is decorated with @keep_name, which means its original name will be maintained when accessed.
  • When print(load_page.__name__) is executed, it outputs "load_page" instead of "wrapper", demonstrating the effect of the decorator.

Answer:

text
load_page

Output 15: Dictionary View

python
scores = {"a": 1}
keys = scores.keys()

scores["b"] = 2

print(list(keys))

Explanation

  • A dictionary scores is initialized with a single key-value pair: "a": 1.
  • The keys() method is called on the scores dictionary, creating a view object keys that reflects the current keys in the dictionary.
  • A new key-value pair "b": 2 is added to the scores dictionary after obtaining the keys view.
  • The print function outputs the list of keys, which now includes both "a" and "b" due to the dynamic nature of the keys view.

Answer:

text
['a', 'b']

Dictionary views are dynamic.

Solution Key

Practice Solutions: Coding Interview Problems

Each problem uses fresh examples and avoids course-specific problem statements.

Problem 1: Count Value Types

Write a function that receives a list of values and returns how many are positive, negative, and zero.

python
def count_number_types(numbers):
    result = {"positive": 0, "negative": 0, "zero": 0}

    for number in numbers:
        if number > 0:
            result["positive"] += 1
        elif number < 0:
            result["negative"] += 1
        else:
            result["zero"] += 1

    return result

assert count_number_types([3, -1, 0, 9, -8]) == {
    "positive": 2,
    "negative": 2,
    "zero": 1,
}

Explanation

  • The function count_number_types initializes a dictionary to store counts of positive, negative, and zero values.
  • It iterates through each number in the provided list, updating the respective count based on the number's value.
  • The function returns the dictionary containing the counts after processing all numbers.
  • An assertion checks that the function correctly counts the types in a sample list, ensuring its accuracy.

Time complexity: O(n).

Space complexity: O(1).

Problem 2: Remove Duplicate Values While Preserving Order

python
def unique_in_order(values):
    seen = set()
    result = []

    for value in values:
        if value not in seen:
            seen.add(value)
            result.append(value)

    return result

assert unique_in_order(["py", "sql", "py", "git"]) == ["py", "sql", "git"]

Explanation

  • The function unique_in_order takes a list values as input.
  • It initializes an empty set seen to track unique values and an empty list result to store the filtered values.
  • It iterates through each value in the input list, checking if it has already been encountered.
  • If a value is not in seen, it adds the value to both seen and result, ensuring duplicates are excluded.
  • Finally, the function returns the result list containing only unique values in their original order.

Time complexity: O(n) average.

Space complexity: O(n).

Problem 3: First Repeated Item

python
def first_repeated(values):
    seen = set()

    for value in values:
        if value in seen:
            return value

        seen.add(value)

    return None

assert first_repeated([4, 7, 2, 7, 4]) == 7
assert first_repeated([1, 2, 3]) is None

Explanation

  • The function first_repeated takes a list of values as input and initializes an empty set called seen to track unique elements.
  • It iterates through each value in the input list, checking if the value is already present in the seen set.
  • If a value is found in seen, it is returned immediately as the first repeated element.
  • If no repeated elements are found during the iteration, the function returns None.
  • The assertions at the end test the function with two cases: one with a repeated element and one without any repetitions.

Problem 4: Most Frequent Word

python
def most_frequent_word(text):
    counts = {}

    for raw_word in text.lower().split():
        word = raw_word.strip(".,!?")
        counts[word] = counts.get(word, 0) + 1

    best_word = None
    best_count = 0

    for word, count in counts.items():
        if count > best_count:
            best_word = word
            best_count = count

    return best_word, best_count

assert most_frequent_word("Python is fun. Python is clear.") == ("python", 2)

Explanation

  • The function most_frequent_word takes a string input text and counts the occurrences of each word.
  • It converts the text to lowercase and splits it into individual words, stripping punctuation marks for accurate counting.
  • A dictionary counts is used to store each word as a key and its count as the value.
  • The function iterates through the dictionary to find the word with the highest count, storing it in best_word along with its count in best_count.
  • Finally, it returns the most frequent word and its count, as demonstrated by the assertion test.

Problem 5: Rotate a List Left by k Positions

python
def rotate_left(values, steps):
    if not values:
        return []

    steps = steps % len(values)
    return values[steps:] + values[:steps]

assert rotate_left([1, 2, 3, 4, 5], 2) == [3, 4, 5, 1, 2]

Explanation

  • The function rotate_left takes a list values and an integer steps as input.
  • It first checks if the list is empty; if so, it returns an empty list.
  • The number of steps is adjusted using modulo operation to handle cases where steps exceed the list length.
  • The list is then sliced into two parts: from steps to the end and from the start to steps, and these parts are concatenated to achieve the rotation.
  • An assertion tests the function to ensure it correctly rotates the list [1, 2, 3, 4, 5] by 2 steps, resulting in [3, 4, 5, 1, 2].

Problem 6: Pair Sum Exists

python
def has_pair_sum(numbers, target):
    seen = set()

    for number in numbers:
        needed = target - number

        if needed in seen:
            return True

        seen.add(number)

    return False

assert has_pair_sum([8, 3, 5, 2], 7) is True
assert has_pair_sum([8, 3, 5, 2], 20) is False

Explanation

  • The function has_pair_sum takes a list of integers numbers and an integer target as inputs.
  • It initializes an empty set seen to keep track of numbers encountered during iteration.
  • For each number in the list, it calculates the needed value that, when added to the current number, equals the target.
  • If the needed value is found in the seen set, the function returns True, indicating a valid pair exists.
  • If no such pair is found after checking all numbers, the function returns False.

Time complexity: O(n).

Problem 7: Return Pair Sum Indices

python
def pair_sum_indices(numbers, target):
    positions = {}

    for index, number in enumerate(numbers):
        needed = target - number

        if needed in positions:
            return positions[needed], index

        positions[number] = index

    return None

assert pair_sum_indices([10, 4, 6, 8], 14) == (0, 1)

Explanation

  • The function pair_sum_indices takes a list of numbers and a target sum as input.
  • It uses a dictionary positions to store the indices of numbers encountered during iteration.
  • For each number, it calculates the difference needed to reach the target and checks if this difference exists in the dictionary.
  • If a match is found, it returns the indices of the two numbers that sum to the target.
  • If no such pair exists, the function returns None.

Problem 8: Merge Two Score Dictionaries

If a key appears in both dictionaries, add the values.

python
def merge_scores(first, second):
    merged = first.copy()

    for name, score in second.items():
        merged[name] = merged.get(name, 0) + score

    return merged

assert merge_scores({"a": 2, "b": 1}, {"a": 3, "c": 5}) == {
    "a": 5,
    "b": 1,
    "c": 5,
}

Explanation

  • The function merge_scores takes two dictionaries, first and second, as input parameters.
  • It creates a copy of the first dictionary to avoid modifying the original data.
  • It iterates through each key-value pair in the second dictionary, adding the score to the corresponding key in the merged dictionary.
  • If a key from the second dictionary does not exist in the first, it initializes the score to zero before adding.
  • The function returns the merged dictionary, which contains the combined scores from both input dictionaries.

Problem 9: Group Items by First Letter

python
def group_by_first_letter(words):
    grouped = {}

    for word in words:
        if not word:
            continue

        key = word[0].lower()
        grouped.setdefault(key, []).append(word)

    return grouped

assert group_by_first_letter(["Python", "patterns", "SQL"]) == {
    "p": ["Python", "patterns"],
    "s": ["SQL"],
}

Explanation

  • The function group_by_first_letter takes a list of words as input and initializes an empty dictionary grouped to store the results.
  • It iterates through each word in the input list, skipping any empty strings.
  • For each non-empty word, it converts the first letter to lowercase and uses it as a key in the grouped dictionary, appending the word to the corresponding list.
  • The setdefault method is used to create a new list if the key does not already exist, ensuring that words are grouped correctly.
  • Finally, the function returns the grouped dictionary, which contains lists of words organized by their first letters.

Problem 10: Count Character Runs

Return consecutive character counts.

python
def character_runs(text):
    if not text:
        return []

    result = []
    current = text[0]
    count = 1

    for character in text[1:]:
        if character == current:
            count += 1
        else:
            result.append((current, count))
            current = character
            count = 1

    result.append((current, count))
    return result

assert character_runs("aaabbc") == [("a", 3), ("b", 2), ("c", 1)]

Explanation

  • The function character_runs takes a string input and returns a list of tuples, each containing a character and its consecutive count.
  • It initializes an empty list result to store the character counts and sets the first character as current with an initial count of 1.
  • It iterates through the string starting from the second character, comparing each character with current. If they match, it increments the count; otherwise, it appends the current character and its count to result and updates current and count.
  • After the loop, it appends the last character and its count to result before returning the final list.
  • The assertion checks that the function correctly processes the input "aaabbc" to yield the expected output of character counts.

Problem 11: Encode Character Runs

python
def encode_runs(text):
    parts = []

    for character, count in character_runs(text):
        parts.append(f"{count}{character}")

    return "".join(parts)

assert encode_runs("aaabbc") == "3a2b1c"

Explanation

  • The encode_runs function takes a string text as input and initializes an empty list parts to store encoded segments.
  • It iterates over the output of the character_runs function, which presumably returns tuples of characters and their consecutive counts.
  • For each character and its count, it appends a formatted string (count followed by the character) to the parts list.
  • Finally, it joins all elements in parts into a single string and returns it.
  • The assertion checks that the function correctly encodes the input "aaabbc" to "3a2b1c".

Problem 12: Check Balanced Brackets

python
def has_balanced_brackets(text):
    pairs = {")": "(", "]": "[", "}": "{"}
    openings = set(pairs.values())
    stack = []

    for character in text:
        if character in openings:
            stack.append(character)
        elif character in pairs:
            if not stack or stack[-1] != pairs[character]:
                return False

            stack.pop()

    return not stack

assert has_balanced_brackets("{[()]}") is True
assert has_balanced_brackets("{[(])}") is False

Explanation

  • The function has_balanced_brackets takes a string input and checks for balanced parentheses, brackets, and braces.
  • It uses a dictionary pairs to map closing brackets to their corresponding opening brackets and a set openings to store the opening brackets.
  • A stack is employed to keep track of opening brackets as they are encountered in the string.
  • For each character in the input string, if it is an opening bracket, it is pushed onto the stack; if it is a closing bracket, the function checks if it matches the last opening bracket in the stack.
  • The function returns True if all brackets are balanced (i.e., the stack is empty at the end) and False otherwise, as demonstrated by the assertions.

Problem 13: Matrix Row Maximums

python
def row_maximums(matrix):
    result = []

    for row in matrix:
        if not row:
            result.append(None)
        else:
            result.append(max(row))

    return result

assert row_maximums([[3, 1], [9, 4], []]) == [3, 9, None]

Explanation

  • The function row_maximums takes a 2D list (matrix) as input and initializes an empty list result to store the maximum values.
  • It iterates through each row in the matrix; if a row is empty, it appends None to the result list.
  • For non-empty rows, it calculates the maximum value using the max() function and appends it to the result list.
  • Finally, the function returns the result list containing the maximum values for each row, including None for any empty rows.
  • An assertion is included to verify that the function works correctly with a sample input.

Problem 14: Transpose a Matrix

python
def transpose(matrix):
    if not matrix:
        return []

    column_count = len(matrix[0])
    result = []

    for column_index in range(column_count):
        column = []

        for row in matrix:
            column.append(row[column_index])

        result.append(column)

    return result

assert transpose([[1, 2, 3], [4, 5, 6]]) == [[1, 4], [2, 5], [3, 6]]

Explanation

  • The function transpose takes a 2D list (matrix) as input and returns its transposed version.
  • It first checks if the input matrix is empty; if so, it returns an empty list.
  • The number of columns in the original matrix is determined using len(matrix[0]).
  • A nested loop iterates through each column index and each row to build the transposed columns.
  • Finally, the transposed matrix is returned, and an assertion checks the correctness of the output.

Problem 15: Flatten a Nested List

python
def flatten(values):
    result = []

    for value in values:
        if isinstance(value, list):
            result.extend(flatten(value))
        else:
            result.append(value)

    return result

assert flatten([1, [2, [3, 4]], 5]) == [1, 2, 3, 4, 5]

Explanation

  • The flatten function takes a list values as input and initializes an empty list result to store flattened values.
  • It iterates through each item in values, checking if the item is a list using isinstance.
  • If an item is a list, the function calls itself recursively to flatten that sublist and extends result with the returned values.
  • If the item is not a list, it appends the item directly to result.
  • The function ultimately returns a single flattened list, as demonstrated by the assertion that checks the output against the expected result.

Problem 16: Recursive Sum

python
def recursive_sum(numbers):
    if not numbers:
        return 0

    return numbers[0] + recursive_sum(numbers[1:])

assert recursive_sum([1, 2, 3, 4]) == 10

Explanation

  • The function recursive_sum takes a list of numbers as input.
  • It checks if the list is empty; if so, it returns 0, which serves as the base case for recursion.
  • If the list is not empty, it adds the first element of the list to the result of a recursive call with the rest of the list.
  • The assertion tests the function with a sample list [1, 2, 3, 4], confirming that the output is 10, which is the correct sum.

This version is easy to understand, but slicing creates extra lists. For large lists, an index-based version is better.

Problem 17: Index-Based Recursive Sum

python
def recursive_sum_from(numbers, index=0):
    if index == len(numbers):
        return 0

    return numbers[index] + recursive_sum_from(numbers, index + 1)

assert recursive_sum_from([1, 2, 3, 4]) == 10

Explanation

  • The function recursive_sum_from takes a list numbers and an optional index parameter, defaulting to 0.
  • It checks if the current index is equal to the length of the list; if so, it returns 0, which serves as the base case for recursion.
  • If not at the end of the list, it adds the current number at index to the result of a recursive call with the next index (index + 1).
  • The assertion tests the function with a list [1, 2, 3, 4], confirming that the sum calculated is 10.
python
def binary_search(sorted_values, target):
    left = 0
    right = len(sorted_values) - 1

    while left <= right:
        middle = (left + right) // 2

        if sorted_values[middle] == target:
            return middle

        if sorted_values[middle] < target:
            left = middle + 1
        else:
            right = middle - 1

    return -1

assert binary_search([2, 5, 8, 12], 8) == 2
assert binary_search([2, 5, 8, 12], 7) == -1

Explanation

  • Defines a function binary_search that takes a sorted list and a target value as inputs.
  • Initializes two pointers, left and right, to represent the current search boundaries.
  • Uses a while loop to repeatedly narrow down the search range until the target is found or the range is exhausted.
  • Calculates the middle index and compares the middle value with the target to determine the next search direction.
  • Returns the index of the target if found, or -1 if the target is not present in the list.

Time complexity: O(log n).

Problem 19: Maximum Subarray Sum

python
def maximum_subarray_sum(numbers):
    if not numbers:
        raise ValueError("numbers must not be empty")

    best = numbers[0]
    current = numbers[0]

    for number in numbers[1:]:
        current = max(number, current + number)
        best = max(best, current)

    return best

assert maximum_subarray_sum([-2, 4, -1, 3, -5]) == 6

Explanation

  • The function maximum_subarray_sum takes a list of integers as input and raises an error if the list is empty.
  • It initializes two variables, best and current, to the first element of the list to track the maximum sum found so far and the current subarray sum.
  • The function iterates through the list starting from the second element, updating current to be either the current number or the sum of current and the current number, whichever is larger.
  • It updates best whenever current exceeds its value, ensuring it always holds the maximum contiguous subarray sum.
  • The final result is returned, and an assertion checks that the function correctly computes the maximum subarray sum for a given test case.

Problem 20: Product Except Current Index

python
def product_except_current(numbers):
    result = [1] * len(numbers)

    prefix = 1
    for index, number in enumerate(numbers):
        result[index] = prefix
        prefix *= number

    suffix = 1
    for index in range(len(numbers) - 1, -1, -1):
        result[index] *= suffix
        suffix *= numbers[index]

    return result

assert product_except_current([2, 3, 4]) == [12, 8, 6]

Explanation

  • The function product_except_current takes a list of numbers and returns a new list where each element is the product of all other elements in the input list, excluding the current element.
  • It initializes a result list filled with ones, which will store the final products.
  • The first loop calculates the prefix products for each element, storing the cumulative product of all previous elements in the result list.
  • The second loop calculates the suffix products in reverse order, multiplying the current result by the cumulative product of all subsequent elements.
  • The final output is a list where each index contains the product of all numbers in the input list except the number at that index, validated by the assertion statement.

Problem 21: Longest Consecutive Streak

python
def longest_consecutive_streak(numbers):
    values = set(numbers)
    best = 0

    for number in values:
        if number - 1 in values:
            continue

        current = number
        length = 1

        while current + 1 in values:
            current += 1
            length += 1

        best = max(best, length)

    return best

assert longest_consecutive_streak([10, 4, 20, 1, 3, 2]) == 4

Explanation

  • The function longest_consecutive_streak takes a list of integers as input and identifies the longest sequence of consecutive numbers.
  • It converts the input list into a set to enable O(1) average time complexity for membership checks.
  • The outer loop iterates through each unique number, skipping those that are not the start of a sequence (i.e., if the previous number exists in the set).
  • An inner while loop counts the length of the consecutive sequence starting from the current number.
  • Finally, it returns the maximum length found among all sequences, as demonstrated by the assertion test.

Problem 22: Longest Substring Without Repeating Characters

python
def longest_unique_substring_length(text):
    last_seen = {}
    start = 0
    best = 0

    for index, character in enumerate(text):
        if character in last_seen and last_seen[character] >= start:
            start = last_seen[character] + 1

        last_seen[character] = index
        best = max(best, index - start + 1)

    return best

assert longest_unique_substring_length("abcaef") == 5

Explanation

  • The function longest_unique_substring_length takes a string text as input and initializes a dictionary last_seen to track the last index of each character.
  • It uses two variables, start to mark the beginning of the current substring and best to store the maximum length found.
  • The function iterates through each character in the string, checking if it has been seen before and is within the current substring range.
  • If a duplicate character is found, start is updated to the index right after the last occurrence of that character.
  • Finally, it calculates the length of the current substring and updates best if this length is greater, returning the maximum length of unique substrings.

Problem 23: Safe Average

python
def safe_average(values):
    if not values:
        raise ValueError("values must not be empty")

    total = 0

    for value in values:
        if not isinstance(value, (int, float)):
            raise TypeError("all values must be numeric")

        total += value

    return total / len(values)

assert safe_average([10, 20, 30]) == 20

Explanation

  • The function safe_average takes a list of values and checks if it is empty, raising a ValueError if so.
  • It initializes a total variable to accumulate the sum of numeric values.
  • The function iterates through each value, checking if it is either an integer or a float, raising a TypeError for any non-numeric values.
  • Finally, it returns the average by dividing the total by the number of values in the list.
  • An assertion is included to verify that the function correctly computes the average of the provided list.

Problem 24: Parse Key-Value Lines

python
def parse_key_value_lines(lines):
    result = {}

    for line in lines:
        if "=" not in line:
            continue

        key, value = line.split("=", 1)
        result[key.strip()] = value.strip()

    return result

assert parse_key_value_lines(["name = Ana", "score=91", "bad line"]) == {
    "name": "Ana",
    "score": "91",
}

Explanation

  • The function parse_key_value_lines takes a list of strings as input, where each string is expected to contain a key-value pair separated by an equals sign (=).
  • It initializes an empty dictionary result to store the parsed key-value pairs.
  • The function iterates through each line in the input list, skipping any line that does not contain an equals sign.
  • For valid lines, it splits the line into a key and a value at the first equals sign, trims any whitespace, and adds them to the result dictionary.
  • Finally, it returns the populated dictionary containing all valid key-value pairs.

Problem 25: Serialize Progress to JSON

python
import json

def progress_to_json(username, completed_lessons):
    payload = {
        "username": username,
        "completed_lessons": completed_lessons,
    }

    return json.dumps(payload, sort_keys=True)

assert progress_to_json("dev", 4) == '{"completed_lessons": 4, "username": "dev"}'

Explanation

  • The function progress_to_json takes two parameters: username and completed_lessons.
  • It creates a dictionary payload containing the user's name and the number of lessons they have completed.
  • The json.dumps method is used to convert the dictionary into a JSON string, with keys sorted for consistency.
  • An assertion checks that the function correctly formats the output for a specific input, ensuring the function behaves as expected.

Problem 26: Chunk Values With a Generator

python
def chunked(values, size):
    if size <= 0:
        raise ValueError("size must be positive")

    for index in range(0, len(values), size):
        yield values[index:index + size]

assert list(chunked([1, 2, 3, 4, 5], 2)) == [[1, 2], [3, 4], [5]]

Explanation

  • The chunked function takes a list values and an integer size as input parameters.
  • It raises a ValueError if the size is less than or equal to zero, ensuring valid input.
  • The function uses a for loop with a step of size to iterate through the list, yielding sublists of the specified size.
  • The yield statement allows the function to return chunks one at a time, making it memory efficient.
  • The assertion tests the function by checking if the output matches the expected list of chunks when the input list is divided into groups of two.

Problem 27: Create a Countdown Iterator

python
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

assert list(Countdown(3)) == [3, 2, 1]

Explanation

  • The Countdown class initializes with a starting number and maintains the current countdown value.
  • The __iter__ method returns the iterator object itself, allowing it to be used in a loop.
  • The __next__ method decrements the current value and returns it until it reaches zero, at which point it raises a StopIteration exception to signal the end of the iteration.
  • The assertion at the end verifies that creating a list from the Countdown iterator with a starting value of 3 produces the expected countdown list [3, 2, 1].

Problem 28: Decorator That Requires a Key

python
import functools

def require_key(key):
    def decorator(function_to_call):
        @functools.wraps(function_to_call)
        def wrapper(payload, *args, **kwargs):
            if key not in payload:
                raise KeyError(f"missing key: {key}")

            return function_to_call(payload, *args, **kwargs)

        return wrapper

    return decorator

@require_key("title")
def publish_payload(payload):
    return payload["title"].strip().title()

assert publish_payload({"title": "python basics"}) == "Python Basics"

Explanation

  • The require_key function is a decorator factory that checks for the presence of a specified key in a given payload.
  • It defines an inner wrapper function that raises a KeyError if the required key is missing from the payload.
  • The decorator is applied to the publish_payload function, which processes the payload by extracting and formatting the value associated with the "title" key.
  • The assertion at the end verifies that the publish_payload function correctly formats the title from the provided payload.

Problem 29: Class for Tracking Scores

python
class ScoreTracker:
    def __init__(self):
        self._scores = []

    def add(self, score):
        if score < 0:
            raise ValueError("score must not be negative")

        self._scores.append(score)

    @property
    def average(self):
        if not self._scores:
            return 0

        return sum(self._scores) / len(self._scores)

tracker = ScoreTracker()
tracker.add(80)
tracker.add(100)

assert tracker.average == 90

Explanation

  • The ScoreTracker class initializes with an empty list to store scores.
  • The add method allows adding a score, raising a ValueError if the score is negative.
  • The average property computes the average of the stored scores, returning 0 if no scores are present.
  • An instance of ScoreTracker is created, and two scores (80 and 100) are added.
  • The average score is asserted to be 90, confirming the correct functionality of the class.

Problem 30: Polymorphic Exporters

python
class TextExporter:
    def export(self, rows):
        return "\n".join(rows)

class CsvExporter:
    def export(self, rows):
        return ",".join(rows)

def export_rows(exporter, rows):
    return exporter.export(rows)

assert export_rows(TextExporter(), ["a", "b"]) == "a\nb"
assert export_rows(CsvExporter(), ["a", "b"]) == "a,b"

Explanation

  • Defines two classes, TextExporter and CsvExporter, each implementing an export method to format data differently.
  • The export_rows function takes an exporter object and a list of rows, calling the appropriate export method based on the provided exporter.
  • The assertions at the end verify that the export_rows function correctly formats the input data for both text and CSV formats.
  • This design allows for easy extension to support additional export formats by creating new exporter classes without modifying existing code.

Problem 31: Abstract Export Contract

python
import abc

class Exporter(abc.ABC):
    @abc.abstractmethod
    def export(self, rows):
        raise NotImplementedError

class JsonLineExporter(Exporter):
    def export(self, rows):
        return "\n".join(rows)

exporter = JsonLineExporter()
assert exporter.export(["one", "two"]) == "one\ntwo"

Explanation

  • The Exporter class is defined as an abstract base class using the abc module, enforcing the implementation of the export method in subclasses.
  • The export method in Exporter is marked as abstract, meaning any subclass must provide its own implementation.
  • The JsonLineExporter class inherits from Exporter and implements the export method to convert a list of rows into a single string with each row on a new line.
  • An instance of JsonLineExporter is created, and the export method is tested with a sample list, asserting that the output matches the expected JSON lines format.

Problem 32: Validate a Password

python
def is_valid_password(password):
    if len(password) < 8:
        return False

    has_digit = any(character.isdigit() for character in password)
    has_alpha = any(character.isalpha() for character in password)

    return has_digit and has_alpha

assert is_valid_password("python3x") is True
assert is_valid_password("short1") is False

Explanation

  • The function is_valid_password verifies that the password is at least 8 characters long.
  • It checks for the presence of at least one digit and one alphabetic character using generator expressions.
  • The function returns True if both conditions are satisfied; otherwise, it returns False.
  • Two assertions test the function: one with a valid password and another with an invalid one.

Problem 33: Convert Snake Case to Title

python
def snake_to_title(name):
    words = name.split("_")
    cleaned = [word.capitalize() for word in words if word]

    return " ".join(cleaned)

assert snake_to_title("python_interview_guide") == "Python Interview Guide"

Explanation

  • The function snake_to_title takes a string name formatted in snake_case as input.
  • It splits the string into individual words using the underscore _ as a delimiter.
  • A list comprehension is used to capitalize each word while filtering out any empty strings.
  • Finally, the cleaned words are joined together with a space to form a title case string.
  • An assertion checks that the function correctly transforms "python_interview_guide" into "Python Interview Guide".

Problem 34: Find Missing Number From 1 to n

python
def missing_number(values, limit):
    expected = limit * (limit + 1) // 2
    actual = sum(values)

    return expected - actual

assert missing_number([1, 2, 4, 5], 5) == 3

Explanation

  • The function missing_number takes a list of integers values and an integer limit as input.
  • It calculates the expected sum of the first limit natural numbers using the formula limit * (limit + 1) // 2.
  • It computes the actual sum of the numbers present in the values list.
  • The function returns the difference between the expected sum and the actual sum, which represents the missing number.
  • An assertion checks that the function correctly identifies the missing number (3) when given the input list [1, 2, 4, 5] and a limit of 5.

Problem 35: Find Second Largest Distinct Value

python
def second_largest_distinct(numbers):
    first = None
    second = None

    for number in numbers:
        if number == first or number == second:
            continue

        if first is None or number > first:
            second = first
            first = number
        elif second is None or number > second:
            second = number

    if second is None:
        raise ValueError("need at least two distinct values")

    return second

assert second_largest_distinct([5, 1, 5, 3]) == 3

Explanation

  • Defines a function second_largest_distinct that takes a list of numbers as input.
  • Initializes two variables, first and second, to track the largest and second largest distinct numbers.
  • Iterates through each number in the list, skipping duplicates and updating first and second as necessary.
  • Raises a ValueError if there are not at least two distinct values in the input list.
  • The function returns the second largest distinct number, as demonstrated by the assertion test.

Problem 36: Build a Small Command Router

python
def start():
    return "starting"

def stop():
    return "stopping"

def unknown():
    return "unknown command"

def run_command(command):
    actions = {
        "start": start,
        "stop": stop,
    }

    action = actions.get(command, unknown)
    return action()

assert run_command("start") == "starting"
assert run_command("pause") == "unknown command"

Explanation

  • The start and stop functions return strings indicating their respective actions.
  • The unknown function returns a default message for unrecognized commands.
  • The run_command function maps command strings to their corresponding functions using a dictionary.
  • If a command is not found in the dictionary, it defaults to the unknown function.
  • The assertions at the end verify that the run_command function behaves as expected for both valid and invalid commands.

Problem 37: Sort Records by Multiple Fields

python
def sort_learners(records):
    return sorted(records, key=lambda item: (-item["score"], item["name"]))

learners = [
    {"name": "Bea", "score": 90},
    {"name": "Ana", "score": 90},
    {"name": "Dev", "score": 80},
]

assert sort_learners(learners) == [
    {"name": "Ana", "score": 90},
    {"name": "Bea", "score": 90},
    {"name": "Dev", "score": 80},
]

Explanation

  • The sort_learners function takes a list of dictionaries, each representing a learner with a name and score.
  • It uses the sorted() function with a custom sorting key defined by a lambda function.
  • The sorting key prioritizes the score in descending order (using -item["score"]) and then the name in ascending order.
  • The function returns a new list of learners sorted according to the specified criteria.
  • An assertion checks that the output matches the expected sorted order for verification.

Problem 38: Implement a Simple Cache Decorator

python
import functools

def cache_by_argument(function_to_call):
    cache = {}

    @functools.wraps(function_to_call)
    def wrapper(argument):
        if argument not in cache:
            cache[argument] = function_to_call(argument)

        return cache[argument]

    return wrapper

@cache_by_argument
def square(number):
    return number * number

assert square(6) == 36

Explanation

  • The cache_by_argument function is a decorator that caches the results of a function based on its input arguments to avoid redundant calculations.
  • A dictionary named cache is used to store results, where the keys are the input arguments and the values are the corresponding outputs of the function.
  • The wrapper function checks if the argument is already in the cache; if not, it computes the result and stores it in the cache.
  • The @functools.wraps decorator is applied to preserve the original function's metadata, such as its name and docstring.
  • The square function, decorated with @cache_by_argument, calculates the square of a number and benefits from caching for improved performance on repeated calls with the same argument.

Problem 39: Readable File Extension Summary

python
from pathlib import Path

def count_extensions(paths):
    counts = {}

    for raw_path in paths:
        suffix = Path(raw_path).suffix.lower() or "<none>"
        counts[suffix] = counts.get(suffix, 0) + 1

    return counts

assert count_extensions(["a.py", "b.PY", "notes"]) == {".py": 2, "<none>": 1}

Explanation

  • The function count_extensions takes a list of file paths as input and initializes an empty dictionary counts to store the frequency of each file extension.
  • It iterates through each path, using Path(raw_path).suffix.lower() to extract the file extension in lowercase, defaulting to "<none>" if there is no extension.
  • The dictionary is updated with the count of each extension, incrementing the count for existing extensions or initializing it to 1 if it's the first occurrence.
  • Finally, the function returns the dictionary containing the counts of each file extension.
  • An assertion tests the function to ensure it correctly counts the extensions for the provided list of file paths.

Problem 40: Build a Tiny Module-Style Main Function

python
def build_message(name):
    return f"Hello, {name}"

def main():
    print(build_message("Python"))

if __name__ == "__main__":  # direct run only
    main()

Explanation

  • The build_message function takes a single parameter, name, and returns a formatted greeting string.
  • The main function calls build_message with the argument "Python" and prints the resulting message.
  • The conditional if __name__ == "__main__": ensures that main is executed only when the script is run directly, not when imported as a module.
  • This structure promotes modularity and reusability of the build_message function in other contexts.

In an interview, explain that main() runs only when the file is executed directly.

Practice Lab

Practice Exercises

Try these without looking for solutions first.

  1. Write a function that returns the number of vowels in a string.
  2. Write a function that reverses words in a sentence but keeps each word's letters unchanged.
  3. Write a function that checks whether two strings are anagrams.
  4. Write a function that returns the first non-repeating character in a string.
  5. Write a function that removes all spaces from a string without using replace().
  6. Write a function that capitalizes each word without using title().
  7. Write a function that finds the longest word in a sentence.
  8. Write a function that counts words case-insensitively.
  9. Write a function that returns common values between two lists without duplicates.
  10. Write a function that returns values present in the first list but not in the second.
  11. Write a function that merges two sorted lists.
  12. Write a function that checks whether a list is sorted in non-decreasing order.
  13. Write a function that moves all zero values to the end of a list.
  14. Write a function that returns leaders in a list, where a leader is greater than all values to its right.
  15. Write a function that returns the kth smallest distinct value.
  16. Write a function that finds the longest run of the same value in a list.
  17. Write a function that groups records by a selected dictionary key.
  18. Write a function that inverts a dictionary whose values are unique.
  19. Write a function that creates a frequency table from a list.
  20. Write a function that sorts dictionary keys by their values.
  21. Write a recursive function that counts digits in a positive integer.
  22. Write a recursive function that checks whether a string is a palindrome.
  23. Write a recursive function that finds the maximum value in a list.
  24. Write a recursive function that counts nested list depth.
  25. Write a generator that yields even numbers up to a limit.
  26. Write a generator that yields running totals from a list.
  27. Write an iterator class that loops over pages of records.
  28. Write a decorator that measures call count.
  29. Write a decorator that rejects empty string arguments.
  30. Write a closure that remembers the last value passed to it.
  31. Write a class for a bank wallet with deposit, withdraw, and balance.
  32. Write a class for a quiz question with answer checking.
  33. Write a class hierarchy for free and premium lessons.
  34. Write an abstract base class for different report renderers.
  35. Write a function that writes a dictionary to a JSON file.
  36. Write a function that reads a JSON file and handles invalid JSON.
  37. Write a function that reads a text file and returns the top five words.
  38. Write a function that validates a list of email-like strings.
  39. Write a command-line menu loop with safe input handling.
  40. Write a small package layout with one module for calculations and one module for display.

Part 5: Rapid Revision Tables

Data Structure Choice

NeedBest fit
Ordered editable sequencelist
Fixed recordtuple
Unique valuesset
Key-value lookupdict
Lazy sequencegenerator
Reusable object behaviorclass

Common Mistakes

MistakeBetter habit
Using mutable default argumentsUse None and create inside the function
Catching bare exceptCatch specific exceptions
Using is for value comparisonUse == for values
Shadowing built-ins like listUse descriptive names like lesson_list
Copying nested lists with shallow copyUse copy.deepcopy() when nested independence matters
Writing code at module top levelPut executable flow inside main()
Using a class for everythingUse a function when there is no state or object behavior
Ignoring complexityExplain runtime and memory tradeoffs

Big O Quick Reference

Code patternCommon complexity
Direct index lookup in a listO(1)
Loop over one listO(n)
Nested loop over same listO(n^2)
Binary search on sorted dataO(log n)
SortingO(n log n)
Dictionary/set membership average caseO(1)

Quick Check

Quick Quiz

  1. What does input() return?
  2. When should you use is instead of ==?
  3. Why are lists not valid dictionary keys?
  4. What does a function return without a return statement?
  5. Why is list.copy() not enough for nested lists?
  6. What are the two required parts of recursion?
  7. What is the difference between yield and return?
  8. Why should decorators use functools.wraps?
  9. What is the purpose of if __name__ == "__main__"?
  10. Why should you avoid loading untrusted pickle files?

Quick Check

Quick Quiz Answers

  1. input() returns a string.
  2. Use is for identity checks, especially is None.
  3. Lists are mutable and unhashable.
  4. It returns None.
  5. A shallow copy still shares nested mutable objects.
  6. A base case and progress toward the base case.
  7. yield pauses and resumes a generator; return ends a function.
  8. It preserves function metadata such as name and docstring.
  9. It separates direct script execution from import behavior.
  10. Pickle can execute unsafe instructions during loading.

Final Interview Checklist

Before an interview, make sure you can:

  • explain Python's typing model
  • trace list aliasing and copying
  • use strings, lists, tuples, sets, and dictionaries confidently
  • write functions with clean parameters and return values
  • avoid mutable default arguments
  • explain recursion with a base case
  • estimate time and space complexity
  • build small classes with __init__, methods, and properties
  • explain inheritance, polymorphism, abstraction, and composition
  • handle files safely with context managers
  • serialize simple data with JSON
  • handle exceptions without hiding bugs
  • explain iterables, iterators, and generators
  • write a simple decorator with functools.wraps
  • explain modules, imports, packages, and __name__

Sources and Further Reading

Frequently Asked Questions

What is Python?
Python is a high-level, general-purpose programming language known for readable syntax and a large standard library, used for tasks such as web development, automation, data analysis, and more.
Why is Python called high-level?
Python is called high-level because it hides many low-level details such as manual memory management and direct CPU instructions, allowing code to be written closer to human-readable logic.
Is Python interpreted or compiled?
Python is commonly called interpreted because users usually run source code directly, but in CPython, the source code is first compiled into bytecode, which is then executed by the Python virtual machine.
What is CPython?
CPython is the reference implementation of Python, written mainly in C, and is what most people refer to when they mention Python unless specifying another implementation like PyPy or Jython.
What is dynamic typing?
Dynamic typing means variable names do not have a fixed type, allowing them to be reassigned to different types of data during execution.

Related Work

See how this thinking shows up in shipped systems.