# Mastering Python Interview Questions: A Comprehensive Guide
URL: https://madhudadi.in/blog/posts/python-interview-questions-complete-guide-for-success
Published: 2026-05-22
Tags: python, interview
Read time: 75 min
Difficulty: beginner
> A complete original Python interview guide covering basics, data structures, functions, recursion, OOP, files, exceptions, iterators, generators, decorators, modules, output tracing, and coding practice.# 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:

| Complexity | Meaning |
|---|---|
| `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:

| Mode | Meaning |
|---|---|
| `r` | read text |
| `w` | write text and replace existing content |
| `a` | append text |
| `rb` | read binary |
| `wb` | write 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.

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


### Problem 18: Binary Search

```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 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

| Need | Best fit |
|---|---|
| Ordered editable sequence | `list` |
| Fixed record | `tuple` |
| Unique values | `set` |
| Key-value lookup | `dict` |
| Lazy sequence | generator |
| Reusable object behavior | class |

### Common Mistakes

| Mistake | Better habit |
|---|---|
| Using mutable default arguments | Use `None` and create inside the function |
| Catching bare `except` | Catch specific exceptions |
| Using `is` for value comparison | Use `==` for values |
| Shadowing built-ins like `list` | Use descriptive names like `lesson_list` |
| Copying nested lists with shallow copy | Use `copy.deepcopy()` when nested independence matters |
| Writing code at module top level | Put executable flow inside `main()` |
| Using a class for everything | Use a function when there is no state or object behavior |
| Ignoring complexity | Explain runtime and memory tradeoffs |

### Big O Quick Reference

| Code pattern | Common complexity |
|---|---|
| Direct index lookup in a list | `O(1)` |
| Loop over one list | `O(n)` |
| Nested loop over same list | `O(n^2)` |
| Binary search on sorted data | `O(log n)` |
| Sorting | `O(n log n)` |
| Dictionary/set membership average case | `O(1)` |

## 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 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

- Python tutorial: https://docs.python.org/3/tutorial/
- Python data model: https://docs.python.org/3/reference/datamodel.html
- Python execution model and naming: https://docs.python.org/3/reference/executionmodel.html
- Built-in types: https://docs.python.org/3/library/stdtypes.html
- Built-in functions: https://docs.python.org/3/library/functions.html
- Exceptions: https://docs.python.org/3/library/exceptions.html
- `json`: https://docs.python.org/3/library/json.html
- `pickle`: https://docs.python.org/3/library/pickle.html
- `pathlib`: https://docs.python.org/3/library/pathlib.html
- `functools`: https://docs.python.org/3/library/functools.html
- `abc`: https://docs.python.org/3/library/abc.html
- `itertools`: https://docs.python.org/3/library/itertools.html
- Python packaging user guide: https://packaging.python.org/
