---
title: "Python Beginner Flashcards"
description: "A beginner flashcard deck for Python fundamentals using spaced repetition: syntax and data types, control flow, functions and scope, lists, dictionaries and sets, string handling, file I/O, exceptions, and the mutability rules that cause most early bugs."
author: "Mohammad Abu Mattar"
canonical: https://mkabumattar.com/flashcards/post/python-beginner-flashcards
---

# Python Beginner Flashcards

The fastest way to stop writing confusing Python is to internalise one sentence: assignment binds a name to an object, and it never copies. Nearly every early bug that feels like the language misbehaving follows from not having that model yet.

_Names, objects, mutability, and which container to reach for_

Work through the deck with that picture in mind. When a card mentions mutability, a default argument, or a copy, it is usually this one idea wearing a different hat. The container comparison at the bottom is worth memorising early: knowing that `in` is a scan on a list and a hash lookup on a set is the most useful performance fact available to a beginner.

## Flashcards

### 1. What is the difference between a list and a tuple?

A list is mutable and written with square brackets; a tuple is immutable and written with parentheses. Because a tuple cannot change, it can be hashed, so it can be a dictionary key or a set member. Use a tuple for a fixed record of related values and a list for a sequence you will modify.

```
nums = [1, 2, 3]      # list: nums.append(4) works
point = (10, 20)      # tuple: point[0] = 5 raises TypeError
seen = {(10, 20)}     # a tuple can go in a set; a list cannot
```

### 2. What does `b = a` do when `a` is a list?

It binds a second name to the same list object; it does not copy. Mutating through either name is visible through both. To copy, use `a.copy()`, `list(a)` or `a[:]`, and note all three copy only the outer list, not the objects inside it.

```
a = [1, 2, 3]
b = a
b.append(4)
print(a)          # [1, 2, 3, 4]
c = a.copy()      # now a separate list
```

### 3. Why is `def f(items=[])` dangerous?

The default value is evaluated once, when the function is defined, so every call that omits the argument shares the same list. Appending to it accumulates across calls. Use `None` as the default and create the list inside the function.

```
def bad(item, bag=[]):
    bag.append(item); return bag
bad(1)   # [1]
bad(2)   # [1, 2]   <- same list

def good(item, bag=None):
    if bag is None: bag = []
    bag.append(item); return bag
```

### 4. What is the difference between `is` and `==`?

`==` compares values by calling `__eq__`; `is` compares identity, asking whether two names refer to the same object. Use `is` only with singletons: `None`, `True` and `False`. Comparing strings or numbers with `is` may appear to work because of interning and then fail for other values.

```
a = [1, 2]; b = [1, 2]
a == b     # True  (same value)
a is b     # False (different objects)
x = None
if x is None: ...   # correct idiom
```

### 5. When should you use a set instead of a list?

When you need fast membership tests or automatic deduplication and do not care about order. Checking `x in a_list` scans every element; `x in a_set` is a hash lookup. Converting a list to a set before a loop of membership checks is often the single biggest easy speedup in beginner code.

```
names = ["ada", "alan", "ada"]
unique = set(names)        # {"ada", "alan"}
"ada" in unique           # fast
```

### 6. What are the three ways to format a string, and which should you use?

f-strings (`f"{name}"`), `str.format()`, and the old `%` operator. Use f-strings: they are the fastest and the most readable, and they support inline expressions and format specs. The others exist mostly in older code.

```
name, n = "Ada", 3
f"{name} has {n} items"          # preferred
f"{n / 7:.2f}"                   # format spec: 0.43
f"{name=}"                       # debug form: name='Ada'
```

### 7. What does a list comprehension do, and when is a loop better?

It builds a new list from an iterable in one expression, and is usually clearer and faster than an append loop. Use a plain loop when the body has side effects, needs multiple statements, or when the comprehension would need more than one condition and a nested loop to read clearly.

```
squares = [x * x for x in range(5)]            # [0, 1, 4, 9, 16]
evens   = [x for x in range(10) if x % 2 == 0]
pairs   = {k: v for k, v in items}             # dict comprehension
```

### 8. What is the difference between a generator expression and a list comprehension?

A list comprehension builds the whole list in memory immediately. A generator expression, written with parentheses, produces values one at a time on demand. Use a generator when you are iterating once, especially over something large, because memory stays constant.

```
sum([x * x for x in range(10_000_000)])   # builds the whole list
sum(x * x for x in range(10_000_000))     # streams, constant memory
```

### 9. How do you iterate over a dictionary correctly?

Iterating the dict directly yields keys. Use `.values()` for values and `.items()` for key-value pairs, which is what you want most of the time. Do not add or remove keys while iterating, or you get a RuntimeError.

```
d = {"a": 1, "b": 2}
for k in d: ...              # keys
for v in d.values(): ...     # values
for k, v in d.items(): ...   # both
```

### 10. What does `dict.get(key, default)` do that `dict[key]` does not?

It returns the default instead of raising `KeyError` when the key is absent. Use `[]` when a missing key is a bug you want to hear about, and `.get()` when absence is expected. `setdefault` and `collections.defaultdict` cover the case where you want to insert the default too.

```
d = {"a": 1}
d["z"]              # KeyError
d.get("z")          # None
d.get("z", 0)       # 0
d.setdefault("z", []).append(1)   # inserts and returns
```

### 11. What is the difference between `append` and `extend`?

`append` adds its argument as a single element, so appending a list gives you a nested list. `extend` iterates the argument and adds each element individually. `+=` on a list behaves like `extend`.

```
a = [1, 2]
a.append([3, 4])    # [1, 2, [3, 4]]
b = [1, 2]
b.extend([3, 4])    # [1, 2, 3, 4]
```

### 12. What does slicing return, and does it copy?

`seq[start:stop:step]` returns a new sequence, with `stop` excluded. For a list it is a shallow copy: the new list is independent, but it holds the same objects, so mutating a nested list affects both. Negative indices count from the end, and `seq[::-1]` reverses.

```
s = [0, 1, 2, 3, 4]
s[1:3]      # [1, 2]     stop excluded
s[-2:]      # [3, 4]
s[::-1]     # [4, 3, 2, 1, 0]
s[:]        # shallow copy of the whole list
```

### 13. What is the correct way to open a file?

Use `with open(...) as f:`. The `with` block closes the file automatically, including when an exception is raised. Always pass `encoding="utf-8"` explicitly, because the platform default differs between systems and produces bugs that only appear on someone else's machine.

```
with open("data.txt", encoding="utf-8") as f:
    for line in f:          # streams, does not load the whole file
        print(line.rstrip())
# file is closed here, even if the loop raised
```

### 14. What is the difference between `try/except/else/finally`?

`try` holds the risky code. `except` runs on a matching exception. `else` runs only if no exception occurred, which keeps unrelated code out of the `try`. `finally` always runs, exception or not, and is where cleanup goes.

```
try:
    value = int(text)
except ValueError:
    print("not a number")
else:
    print("parsed", value)   # only on success
finally:
    print("always runs")
```

### 15. Why is a bare `except:` a bad idea?

It catches everything, including `KeyboardInterrupt` and `SystemExit`, so it swallows your own Ctrl+C and hides real bugs. Catch the specific exception you can handle. If you must be broad, use `except Exception`, which leaves the control-flow exceptions alone.

```
try:
    risky()
except ValueError as err:      # specific: good
    log(err)
except Exception as err:       # broad but safe-ish
    log(err); raise            # re-raise if you cannot handle it
```

### 16. What is the difference between a function argument and a keyword argument?

Positional arguments are matched by order; keyword arguments are matched by name and may be given in any order. `*args` collects extra positional arguments into a tuple and `**kwargs` collects extra keyword arguments into a dict. A `*` in the signature forces everything after it to be keyword-only.

```
def f(a, b=2, *args, key=None, **kwargs): ...
f(1, 3, 4, 5, key="x", other=1)

def g(data, *, verbose=False): ...
g([1], verbose=True)     # g([1], True) is a TypeError
```

### 17. What is the difference between local and global scope?

A name assigned inside a function is local to it. Reading an outer name works without declaration, but assigning to one creates a new local unless you declare `global` (module level) or `nonlocal` (enclosing function). Needing `global` is usually a sign the value should be a parameter or a return value.

```
count = 0
def bump():
    global count      # without this, "count += 1" raises
    count += 1
```

### 18. What does `if __name__ == "__main__":` do?

It runs the block only when the file is executed directly, not when it is imported. `__name__` is `"__main__"` for the script you ran and the module name otherwise. Without it, importing your module to reuse one function also runs all your top-level code.

```
def main(): ...

if __name__ == "__main__":
    main()
```

### 19. What is the difference between `range(5)`, `range(1, 5)` and `range(0, 10, 2)`?

`range(stop)` counts from 0; `range(start, stop)` from start; `range(start, stop, step)` in steps. The stop value is always excluded. A range is lazy, so it does not build a list, which is why `range(10**9)` is instant.

```
list(range(5))         # [0, 1, 2, 3, 4]
list(range(1, 5))      # [1, 2, 3, 4]
list(range(0, 10, 2))  # [0, 2, 4, 6, 8]
list(range(5, 0, -1))  # [5, 4, 3, 2, 1]
```

### 20. How do `enumerate` and `zip` help in loops?

`enumerate` yields index-value pairs, so you do not need a manual counter or `range(len(x))`. `zip` walks several iterables together and stops at the shortest one. Both are lazy.

```
for i, name in enumerate(names, start=1):
    print(i, name)

for name, score in zip(names, scores):
    print(name, score)
```

### 21. What is truthiness in Python?

Any object can be tested in a boolean context. Falsy values are `None`, `False`, zero of any numeric type, and empty containers such as `""`, `[]`, `{}`, `()`. Everything else is truthy. This is why `if items:` is preferred over `if len(items) > 0:`.

```
if not items:  ...      # empty list, tuple, dict, str
if value is None: ...   # NOT the same as "if not value"
# 0 and "" are falsy but not None, which matters for defaults
```

### 22. What is the difference between `sort()` and `sorted()`?

`list.sort()` sorts in place and returns `None`. `sorted(iterable)` returns a new list and works on anything iterable. Assigning the result of `.sort()` is a common beginner bug because it gives you `None`.

```
nums = [3, 1, 2]
nums.sort()                 # nums is now [1, 2, 3]
x = nums.sort()             # x is None  <- bug
y = sorted(nums, reverse=True)   # new list
words.sort(key=len)         # sort by a computed value
```

### 23. What does `*` do when calling a function, as opposed to defining one?

In a call it unpacks: `f(*my_list)` passes the elements as separate positional arguments, and `f(**my_dict)` passes entries as keyword arguments. In a definition it collects the extras instead. Same symbol, opposite direction.

```
def add(a, b): return a + b
args = [1, 2]
add(*args)                  # same as add(1, 2)
kw = {"a": 1, "b": 2}
add(**kw)                   # same as add(a=1, b=2)
```

### 24. What is a docstring and where does it go?

A string literal as the first statement of a module, function or class. It is stored on the object as `__doc__` and is what `help()` shows. It differs from a comment in being available at runtime and being the thing documentation tools read.

```
def area(r):
    """Return the area of a circle with radius r."""
    return 3.14159 * r ** 2

help(area)          # shows the docstring
```

### 25. Why does `0.1 + 0.2 == 0.3` return False?

Floats are binary approximations, and 0.1 cannot be represented exactly, so small errors accumulate. Compare with a tolerance using `math.isclose`, and use `decimal.Decimal` for money, where exactness matters more than speed.

```
0.1 + 0.2            # 0.30000000000000004
import math
math.isclose(0.1 + 0.2, 0.3)      # True
from decimal import Decimal
Decimal("0.1") + Decimal("0.2")   # Decimal('0.3')
```

### 26. What is the difference between a module and a package?

A module is one `.py` file. A package is a directory of modules, historically marked by an `__init__.py`. You import from both the same way. The file name becomes the module name, which is why naming a file `random.py` shadows the standard library and breaks imports confusingly.

```
import math                 # module
from pathlib import Path    # name from a module
import os.path              # submodule of a package
```

### 27. What does `pathlib` give you over string paths?

Path objects with methods and an operator for joining, which removes manual separator handling and works the same on Windows. It is the modern replacement for most of `os.path`.

```
from pathlib import Path
p = Path("data") / "raw" / "file.txt"   # correct separator anywhere
p.exists(); p.suffix; p.stem
p.read_text(encoding="utf-8")
p.parent.mkdir(parents=True, exist_ok=True)
```

### 28. How do you define a simple class, and what is `self`?

`class Name:` with an `__init__` for setup. `self` is the instance, passed automatically when you call a method on an object, and declared explicitly in the definition. `__repr__` is worth adding early because it is what you see when debugging.

```
class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __repr__(self):
        return f"Point({self.x}, {self.y})"

p = Point(1, 2)      # __init__ runs; self is p
```

### 29. What is the difference between a class attribute and an instance attribute?

A class attribute is defined in the class body and shared by every instance. An instance attribute is assigned on `self` and belongs to one object. A mutable class attribute is the same trap as a mutable default argument: every instance shares it.

```
class Bad:
    items = []            # SHARED by all instances
class Good:
    def __init__(self):
        self.items = []   # one per instance
```

### 30. What does `len()` work on, and what does it not?

Anything implementing `__len__`: strings, lists, tuples, dicts, sets. It does not work on a generator or an integer, because neither has a length; a generator does not know how many items it will produce until it produces them.

```
len("abc"); len([1, 2]); len({"a": 1})
len(x for x in range(3))    # TypeError
sum(1 for _ in gen)         # count a generator, consuming it
```

### 31. How do you split and join strings?

`s.split(sep)` returns a list, splitting on whitespace and collapsing runs of it when called with no argument. `sep.join(iterable)` joins strings, and the separator is the string you call it on, which reads backwards at first. `join` requires every element to be a string already.

```
"a,b,c".split(",")        # ["a", "b", "c"]
"  a  b ".split()         # ["a", "b"]  collapses whitespace
", ".join(["a", "b"])     # "a, b"
", ".join([1, 2])         # TypeError: needs str
```

### 32. What is the difference between `str` and `bytes`?

`str` is text, a sequence of Unicode characters. `bytes` is binary data. Converting between them requires an encoding: `.encode()` goes text to bytes, `.decode()` goes back. Files opened in binary mode give you bytes, which is why mixing the two raises TypeError.

```
"café".encode("utf-8")            # b'caf\xc3\xa9'  (5 bytes)
b"caf\xc3\xa9".decode("utf-8")    # "café"          (4 chars)
len("café"), len("café".encode()) # (4, 5)
```

### 33. What does `None` mean, and how should you test for it?

It is the single object representing "no value", the default return of a function with no `return`. Test with `is None` rather than `== None` or truthiness, because `0` and `""` are falsy but are not `None`, and conflating them is a real source of bugs.

```
def f(): pass
f() is None            # True
value = 0
if not value: ...      # True, but value is NOT None
if value is None: ...  # False, the correct check
```

### 34. What is the difference between `import x` and `from x import y`?

`import x` binds the module name, so you use `x.y`. `from x import y` binds `y` directly. The second is shorter but loses the origin at the call site, and `from x import *` is discouraged because it makes it impossible to tell where a name came from.

```
import json
json.dumps(obj)

from json import dumps
dumps(obj)                # origin no longer visible here
```

### 35. How do you write and read a JSON file?

`json.dump`/`json.load` work with a file object; `json.dumps`/`json.loads` work with strings (the `s` is for string). Only JSON-compatible types survive the round trip: a tuple comes back as a list, and a `datetime` needs a custom encoder.

```
import json
with open("out.json", "w", encoding="utf-8") as f:
    json.dump({"a": (1, 2)}, f)      # tuple written as a list
with open("out.json", encoding="utf-8") as f:
    data = json.load(f)              # {"a": [1, 2]}
```

### 36. What is a virtual environment and why does it matter?

An isolated directory with its own Python and installed packages, so two projects can require different versions of the same library without conflicting. Installing into the system Python risks breaking operating system tools that depend on it.

```
python -m venv .venv
source .venv/bin/activate      # Windows: .venv\Scripts\activate
pip install requests
pip freeze > requirements.txt
```

### 37. What does `assert` do, and when should you not use it?

It raises `AssertionError` if the condition is false, which suits internal sanity checks and tests. Do not use it to validate untrusted input or enforce security, because Python removes every assert when run with `-O`, and your check silently disappears.

```
def mean(values):
    assert values, "expected a non-empty sequence"   # internal check
    return sum(values) / len(values)
# For user input, raise explicitly instead:
if not values: raise ValueError("values must not be empty")
```

### 38. What is the difference between `//`, `/` and `%`?

`/` is true division and always returns a float, even for whole results. `//` is floor division, rounding toward negative infinity. `%` is the remainder, and in Python it takes the sign of the divisor, which differs from C and surprises people porting code.

```
7 / 2      # 3.5   always a float
7 // 2     # 3
-7 // 2    # -4    floors, not truncates
-7 % 2     # 1     sign follows the divisor
```

### 39. How do you handle a value that might be one of several types?

Prefer designing it out. When you cannot, `isinstance(x, (int, float))` checks against a tuple of types. Prefer `isinstance` over `type(x) == T` because it respects subclasses, and prefer duck typing (try the operation) over checking types up front where it reads clearly.

```
isinstance(3, (int, float))     # True
isinstance(True, int)           # True: bool subclasses int!
type(3) == int                  # works, but ignores subclasses
```

### 40. What is the walrus operator `:=` for?

Assigning inside an expression, so you can use a value you have just computed without a separate line. It is most useful in a `while` condition or a comprehension filter where you would otherwise call the same function twice.

```
while (line := f.readline()):
    process(line)

# avoids calling expensive() twice
results = [y for x in data if (y := expensive(x)) is not None]
```
