Chapter 5 Quiz: Python Essentials for Vibe Coders

Test your understanding of the Python essentials covered in this chapter. Each question has a hidden answer -- try to answer first, then expand the solution to check your work.


Question 1: What is the output of the following code?

x = [1, 2, 3]
y = x
y.append(4)
print(len(x))
Show Answer **4** Lists are mutable objects, and `y = x` does not create a copy -- it creates a second reference to the same list. When you append to `y`, you are also modifying `x` because they point to the same object in memory. To create an independent copy, use `y = x.copy()` or `y = x[:]`.

Question 2: Which of the following values are "falsy" in Python (evaluate to False in a boolean context)? Select all that apply.

a) 0 b) "0" c) [] d) [0] e) "" f) None g) False h) {}

Show Answer **a, c, e, f, g, h** The falsy values are: `0`, `[]` (empty list), `""` (empty string), `None`, `False`, and `{}` (empty dict). The string `"0"` is truthy because it is a non-empty string. The list `[0]` is truthy because it is a non-empty list (even though it contains a falsy value).

Question 3: What is the difference between == and is in Python?

Show Answer `==` checks for **value equality** (whether two objects have the same value), while `is` checks for **identity** (whether two variables refer to the exact same object in memory). For example:
a = [1, 2, 3]
b = [1, 2, 3]
a == b  # True (same value)
a is b  # False (different objects)
You should use `is` primarily for comparing to `None`: `if x is None:` rather than `if x == None:`.

Question 4: What will this code print?

def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"

print(greet("Alice"))
print(greet("Bob", "Hey"))
print(greet(greeting="Howdy", name="Charlie"))
Show Answer
Hello, Alice!
Hey, Bob!
Howdy, Charlie!
The first call uses the default value for `greeting`. The second call provides both arguments positionally. The third call uses keyword arguments in a different order than the function definition, which is perfectly valid in Python.

Question 5: What is the output of this list slicing operation?

data = [10, 20, 30, 40, 50, 60, 70, 80]
print(data[1:6:2])
Show Answer **`[20, 40, 60]`** The slice `[1:6:2]` means: start at index 1 (value 20), stop before index 6 (value 70 is excluded), step by 2. So it picks indices 1, 3, and 5, giving us `[20, 40, 60]`.

Question 6: What is wrong with this code, and how would you fix it?

def add_to_list(item, items=[]):
    items.append(item)
    return items
Show Answer This is the **mutable default argument** bug. The default list `[]` is created once when the function is defined, not each time the function is called. Subsequent calls without providing `items` will share and mutate the same list. The fix is:
def add_to_list(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items
This creates a new list each time the function is called without an explicit `items` argument.

Question 7: What does the @dataclass decorator automatically generate for a class?

Show Answer The `@dataclass` decorator automatically generates: 1. `__init__` -- A constructor that accepts all annotated fields as parameters 2. `__repr__` -- A string representation showing the class name and all field values 3. `__eq__` -- Equality comparison that checks all fields With additional options, it can also generate `__hash__` (with `frozen=True`), `__lt__`, `__le__`, `__gt__`, `__ge__` (with `order=True`), and `__post_init__` can be defined for additional initialization logic.

Question 8: What is the output of this code?

numbers = [1, 2, 3, 4, 5]
result = [x * 2 for x in numbers if x % 2 != 0]
print(result)
Show Answer **`[2, 6, 10]`** The list comprehension filters for odd numbers (`x % 2 != 0` gives 1, 3, 5) and then doubles each one (2, 6, 10).

Question 9: Explain the difference between try/except/else and try/except/finally.

Show Answer - The `else` block runs only if **no exception** was raised in the `try` block. It is useful for code that should only execute on success. - The `finally` block runs **regardless** of whether an exception occurred. It is used for cleanup code (closing files, releasing resources) that must execute no matter what.
try:
    result = do_something()
except ValueError:
    handle_error()
else:
    use_result(result)    # Only runs if no exception
finally:
    cleanup()             # Always runs
You can use both `else` and `finally` in the same `try` statement.

Question 10: What is the purpose of if __name__ == "__main__": and when does the code inside it execute?

Show Answer This guard checks whether the Python file is being run directly (as the main program) or being imported as a module. - When you run `python myfile.py`, Python sets `__name__` to `"__main__"`, so the code inside the block executes. - When another file does `import myfile`, Python sets `__name__` to `"myfile"`, so the code inside the block does NOT execute. This allows a file to serve both as a runnable script and as an importable module, which is especially useful for testing and reuse.

Question 11: What is the output of this code?

d = {"a": 1, "b": 2, "c": 3}
d["d"] = 4
del d["b"]
print(list(d.keys()))
print(d.get("b", "missing"))
Show Answer
['a', 'c', 'd']
missing
After adding key `"d"` and deleting key `"b"`, the dictionary has keys `"a"`, `"c"`, and `"d"`. The `.get("b", "missing")` call returns the default value `"missing"` because key `"b"` no longer exists.

Question 12: What is the difference between a list and a tuple? When would you use each?

Show Answer - **Lists** are mutable (you can add, remove, and change elements). Use them for collections that change over time, like a shopping cart or a list of search results. - **Tuples** are immutable (once created, they cannot be modified). Use them for fixed collections of related values, like coordinates `(x, y)`, RGB colors `(255, 128, 0)`, or function return values. Tuples can also be used as dictionary keys (because they are hashable), while lists cannot. Tuples have slightly lower memory overhead than lists.

Question 13: What will the following code produce?

for i in range(3):
    for j in range(3):
        if i == j:
            break
    else:
        print(f"No break for i={i}")
Show Answer **No output at all.** For every value of `i` (0, 1, 2), the inner loop encounters `j == i` and hits `break`. Because the inner loop always breaks, the `else` clause never executes. The `for/else` pattern's `else` block only runs when the loop completes without a `break`.

Question 14: What does *args mean in a function definition, and what does **kwargs mean?

Show Answer - `*args` collects any number of **positional** arguments into a **tuple**. For example, `def f(*args)` means you can call `f(1, 2, 3)` and inside the function, `args` will be `(1, 2, 3)`. - `**kwargs` collects any number of **keyword** arguments into a **dictionary**. For example, `def f(**kwargs)` means you can call `f(name="Alice", age=30)` and inside the function, `kwargs` will be `{"name": "Alice", "age": 30}`. These are commonly used in wrapper functions, decorators, and functions that need to pass arguments through to other functions.

Question 15: Which of these is the correct way to check for None?

a) if x == None: b) if x is None: c) if not x: d) if x is not None:

Show Answer **b and d** are the correct idioms. - `if x is None:` checks if `x` is `None` (identity comparison). - `if x is not None:` checks if `x` is not `None`. Option (a) uses `==` which works but is considered non-Pythonic and can be overridden by custom `__eq__` methods. Option (c) checks for "falsyness," which catches `None` but also catches `0`, `""`, `[]`, and other falsy values, which is usually not what you want when specifically checking for `None`.

Question 16: What is the output?

text = "Hello, World!"
print(text[7:12])
print(text[-6:])
print(text[:5])
Show Answer
World
orld!
Hello
- `text[7:12]` extracts characters at indices 7, 8, 9, 10, 11: `"World"`. - `text[-6:]` extracts the last 6 characters: `"orld!"` Wait, let me count. The string is `"Hello, World!"` which is 13 characters. Index -6 is `W`. So `text[-6:]` is `"orld!"`. Actually, let me recount: H(0) e(1) l(2) l(3) o(4) ,(5) (6) W(7) o(8) r(9) l(10) d(11) !(12). Index -6 is 13-6=7, which is `W`. So `text[-6:]` is `"World!"`. Correction:
World
World!
Hello
`text[-6:]` gives `"World!"` (the last 6 characters).

Question 17: What is the difference between .sort() and sorted()?

Show Answer - `.sort()` is a **list method** that sorts the list **in place** (modifies the original list) and returns `None`. - `sorted()` is a **built-in function** that returns a **new sorted list** and leaves the original unchanged.
nums = [3, 1, 4, 1, 5]

# sorted() returns a new list
new_list = sorted(nums)  # new_list = [1, 1, 3, 4, 5], nums unchanged

# .sort() modifies in place
nums.sort()               # nums is now [1, 1, 3, 4, 5], returns None
`sorted()` also works on any iterable (tuples, sets, generators), while `.sort()` only works on lists.

Question 18: Explain what this code does:

from pathlib import Path

config_dir = Path.home() / ".myapp" / "config"
config_dir.mkdir(parents=True, exist_ok=True)
config_file = config_dir / "settings.json"
config_file.write_text('{"debug": false}')
Show Answer This code: 1. Creates a `Path` object pointing to a `.myapp/config` directory inside the user's home directory (e.g., `/home/alice/.myapp/config` on Linux or `C:\Users\Alice\.myapp\config` on Windows). 2. Creates that entire directory structure if it does not already exist. `parents=True` means it will create intermediate directories (`.myapp` and `config`), and `exist_ok=True` means it will not raise an error if the directory already exists. 3. Creates a `Path` object for a `settings.json` file inside that config directory. 4. Writes the JSON string `{"debug": false}` to that file, creating it if it does not exist or overwriting it if it does.

Question 19: What is a generator, and how does it differ from a list comprehension?

Show Answer A **generator** produces values lazily, one at a time, on demand. A **list comprehension** creates the entire list in memory at once.
# List comprehension: all values computed and stored immediately
squares_list = [x**2 for x in range(1000000)]  # Uses lots of memory

# Generator expression: values computed one at a time as needed
squares_gen = (x**2 for x in range(1000000))    # Almost no memory
Generators are created with parentheses `()` instead of brackets `[]`, or with functions that use `yield` instead of `return`. They are ideal for processing large datasets or infinite sequences because they only keep one value in memory at a time. The tradeoff is that generators can only be iterated once, while lists can be accessed multiple times by index.

Question 20: What is the output of this code?

a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
print(a & b)
print(a | b)
print(a - b)
Show Answer
{3, 4}
{1, 2, 3, 4, 5, 6}
{1, 2}
- `a & b` is the **intersection** (elements in both sets): `{3, 4}`. - `a | b` is the **union** (elements in either set): `{1, 2, 3, 4, 5, 6}`. - `a - b` is the **difference** (elements in `a` but not in `b`): `{1, 2}`. Note: set output order may vary since sets are unordered, but the elements will be the same.

Question 21: When reviewing AI-generated code, you see this import at the top of a file:

from quantum_utils import QuantumCircuit

What should you check?

Show Answer You should check whether `quantum_utils` is a **real** package or a **hallucinated** one. AI assistants sometimes invent package names that do not exist. Steps to verify: 1. Search PyPI (pypi.org) for the package name 2. Check if it is a local module in the project (look for a `quantum_utils.py` file or `quantum_utils/` directory) 3. Check `requirements.txt` or `pyproject.toml` to see if it is listed as a dependency 4. Try `pip install quantum_utils` if it appears to be a third-party package If the package does not exist, ask the AI to use a real alternative or remove the dependency.

Question 22: What does this type hint mean?

def process(data: list[dict[str, int | float]]) -> dict[str, float]:
Show Answer This function: - **Accepts** a parameter `data` which is a **list of dictionaries**, where each dictionary has **string keys** and values that are either **int or float**. - **Returns** a **dictionary** with **string keys** and **float values**. For example, valid input would be:
[{"price": 19.99, "quantity": 3}, {"price": 5.50, "quantity": 12}]
And the return value might be:
{"total_price": 25.49, "total_quantity": 15.0}

Question 23: Why should you prefer pathlib.Path over os.path for path operations in modern Python?

Show Answer `pathlib.Path` is preferred because: 1. **Object-oriented interface**: Paths are objects with methods, not plain strings. This makes code more readable and less error-prone. 2. **Operator overloading**: You can use `/` to join paths: `Path("dir") / "file.txt"` instead of `os.path.join("dir", "file.txt")`. 3. **Method chaining**: `path.parent.name` is cleaner than `os.path.basename(os.path.dirname(path))`. 4. **Convenience methods**: `path.read_text()`, `path.write_text()`, `path.glob()`, `path.exists()` are built in. 5. **Cross-platform**: Automatically handles `/` vs `\` path separators. 6. **Modern standard**: `pathlib` was introduced in Python 3.4 and is the recommended approach for new code.

Question 24: What is the output of this code?

def make_multiplier(n):
    def multiplier(x):
        return x * n
    return multiplier

double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5))
print(triple(5))
Show Answer
10
15
This demonstrates a **closure**. `make_multiplier` returns an inner function `multiplier` that "remembers" the value of `n` from its enclosing scope. `double` remembers `n=2` and `triple` remembers `n=3`. When called with `5`, they return `5*2=10` and `5*3=15` respectively. Closures are a common pattern in Python that AI assistants use for creating callback functions, decorators, and factory functions.

Question 25: You are reviewing AI-generated code and encounter a bare except: clause. Why is this problematic, and what should you ask the AI to change it to?

Show Answer A bare `except:` clause catches **all** exceptions, including: - `KeyboardInterrupt` (Ctrl+C to stop the program) - `SystemExit` (calls to `sys.exit()`) - `MemoryError` and other critical system errors This can make the program impossible to stop gracefully and can hide serious bugs. You should ask the AI to: 1. Catch **specific** exceptions: `except ValueError:` or `except (ValueError, TypeError):` 2. At minimum, use `except Exception:` which catches all "normal" exceptions but not `KeyboardInterrupt` and `SystemExit` 3. Log or report the caught exception rather than silently swallowing it Example of what to ask: "Please change the bare `except` to catch specific exception types and log the error."