Data Types & Structures

15 questions found

What is the difference between a Python list and a tuple?

Beginner
A list is MUTABLE (elements can be added, removed, or changed after creation), created with square brackets; a tuple is IMMUTABLE (fixed once created), created with parentheses — tuples are also slightly faster and can be used as dictionary keys or set elements, unlike lists.
my_list = [1, 2, 3]
my_list.append(4)  # OK: lists are mutable

my_tuple = (1, 2, 3)
# my_tuple.append(4)  # Error: tuples have no append method, they're immutable
Real-world example Using a tuple for a fixed (x, y) coordinate pair that should never change, versus a list for a growing shopping cart.

Common follow-ups: Why can a tuple be used as a dictionary key, but a list cannot?

Functions & Scope

What is the difference between a set and a list in terms of uniqueness and ordering?

Beginner
A set stores only UNIQUE elements (duplicates are automatically discarded) and doesn't preserve insertion order in the general case; a list preserves both order AND duplicates — sets also provide much faster O(1) average membership testing (`in`) compared to a list's O(n) linear scan.
unique = {1, 2, 2, 3, 3, 3}
print(unique)  # {1, 2, 3} -- duplicates automatically removed

ordered = [1, 2, 2, 3]
print(ordered)  # [1, 2, 2, 3] -- duplicates and order preserved
Real-world example Deduplicating a large list of user IDs while getting fast O(1) membership checks instead of a slow list scan.

Common follow-ups: Since Python 3.7+, does a regular dict preserve insertion order the same way a list does?

Data Types & Structures

How do you access, add, and remove items from a dictionary?

Beginner
Access a value with dict[key] (raises KeyError if missing) or dict.get(key, default) (returns None or a default instead); add/update with dict[key] = value; remove with del dict[key] or dict.pop(key), the latter also returning the removed value.
person = {"name": "Sam", "age": 30}
print(person["name"])          # 'Sam'
person["city"] = "NYC"          # add a new key
age = person.pop("age")         # remove and return 30
print(person.get("email", "N/A"))  # 'N/A' -- safe default lookup
Real-world example Storing and updating a user's profile information as key-value pairs, safely handling missing fields.

Common follow-ups: What's the difference in behavior between dict[missing_key] and dict.get(missing_key)?

Exception Handling

What's the difference between shallow copy and deep copy for a nested data structure, and which functions perform each?

Intermediate
A shallow copy (via list.copy(), dict.copy(), or copy.copy()) creates a NEW outer container but still references the SAME nested/inner objects — mutating a nested list inside a shallow copy affects the original too. A deep copy (via copy.deepcopy()) recursively copies EVERY nested object, producing a fully independent structure.
import copy

original = [[1, 2], [3, 4]]
shallow = original.copy()
shallow[0].append(99)
print(original)  # [[1, 2, 99], [3, 4]] -- original affected too!

deep = copy.deepcopy(original)
deep[0].append(100)
print(original)  # unaffected by the deep copy's mutation
Real-world example Safely duplicating a nested configuration dictionary before modifying it, without accidentally corrupting the original.

Common follow-ups: Why is a plain assignment (new_list = old_list) NOT even a shallow copy, but something different entirely?

Memory Management & Garbage Collection

How do slicing operations work on a list, including negative indices and step values?

Intermediate
list[start:stop:step] extracts a sub-list from index 'start' (inclusive) up to 'stop' (exclusive), advancing by 'step' — negative indices count from the END of the list, and a negative step reverses direction, letting you write concise expressions like list[::-1] to reverse a list entirely.
numbers = [0, 1, 2, 3, 4, 5]
print(numbers[1:4])    # [1, 2, 3]
print(numbers[-2:])    # [4, 5] -- last two elements
print(numbers[::2])    # [0, 2, 4] -- every other element
print(numbers[::-1])   # [5, 4, 3, 2, 1, 0] -- reversed
Real-world example Extracting a specific range of records or reversing an ordered list of results without a manual loop.

Common follow-ups: Does slicing a list create a NEW list (a copy), or does it return a view into the original like NumPy arrays do?

Iterators & the Iterator Protocol

Why must dictionary keys (and set elements) be hashable, and what makes an object hashable in Python?

Intermediate
Dictionaries and sets use HASHING internally for O(1) average lookup, requiring keys to have a stable, consistent hash value (via __hash__) that doesn't change during the object's lifetime — this is why mutable types like list and dict CAN'T be dictionary keys (their contents, and thus their hash, could change), while immutable types like str, int, and tuple (of hashable elements) can.
valid_dict = {("a", 1): "value"}     # OK: tuple of hashable elements is hashable
# invalid_dict = {["a", 1]: "value"} # TypeError: unhashable type: 'list'
Real-world example Understanding why you must convert a list to a tuple before using it as a dictionary key or adding it to a set.

Common follow-ups: How would you make a CUSTOM class's instances usable as dictionary keys, by implementing __hash__ and __eq__?

Magic Methods & Operator Overloading

What is the difference between collections.deque and a regular list for queue-like operations?

Intermediate
deque (double-ended queue) provides O(1) appends and pops from BOTH ends, while a regular list's insert(0, ...) and pop(0) are O(n) because every remaining element must shift — making deque the correct choice for a FIFO queue or any structure needing efficient operations at the front.
from collections import deque

queue = deque()
queue.append(1)       # O(1): add to the right end
queue.appendleft(0)   # O(1): add to the left end
queue.popleft()        # O(1): remove from the left end

# A regular list's list.pop(0) is O(n) -- much slower for this use case
Real-world example Implementing an efficient task queue or a sliding-window buffer that needs fast operations at both ends.

Common follow-ups: What other useful deque feature lets you cap it at a fixed 'maxlen', automatically discarding old items?

Standard Library Essentials (collections itertools)

How does frozenset differ from a regular set, and what specific use case does its immutability enable?

Advanced
frozenset is an IMMUTABLE version of set — once created, it can't be modified, which (like tuples) makes it HASHABLE and therefore usable as a dictionary key or as an element of another set, something a regular mutable set cannot do.
regular_set = {1, 2, 3}
# hash(regular_set)  # TypeError: unhashable type: 'set'

frozen = frozenset([1, 2, 3])
print(hash(frozen))  # works fine
cache = {frozen: "cached result"}  # usable as a dict key
Real-world example Using a frozenset of tags or permissions as a dictionary key to cache results based on that exact combination.

Common follow-ups: Can you still perform set operations like union() and intersection() on a frozenset, or only mutation methods are disallowed?

Magic Methods & Operator Overloading

How does Python's small integer caching (interning) affect identity comparisons ('is') versus equality comparisons ('==') for integers?

Advanced
CPython pre-caches and reuses integer objects in the range -5 to 256 for performance, so 'is' comparisons between small integers in this range often return True even for separately-created values — but this is an IMPLEMENTATION DETAIL, not a language guarantee, so 'is' should never be relied upon for integer equality; always use '==' for value comparison.
a = 100
b = 100
print(a is b)  # True -- likely, due to small int caching (implementation detail!)

c = 1000
d = 1000
print(c is d)  # often False -- outside the cached range, separate objects

# Always use '==' for value equality, regardless of caching behavior
print(c == d)  # True, reliably
Real-world example Debugging a subtle bug caused by incorrectly using 'is' instead of '==' to compare integer values.

Common follow-ups: Why does CPython specifically choose the range -5 to 256 for this small integer caching optimization?

Memory Management & Garbage Collection

How would you efficiently merge two dictionaries in modern Python, and how do the different approaches handle overlapping keys?

Advanced
Python 3.9+ supports the merge operator (`|`) and update operator (`|=`), and dict unpacking (`{**a, **b}`) also works in earlier versions — in ALL cases, when both dictionaries share a key, the value from the SECOND (right-hand or later) dictionary wins, overwriting the first.
defaults = {"theme": "light", "retries": 3}
overrides = {"theme": "dark"}

merged = defaults | overrides           # Python 3.9+: {'theme': 'dark', 'retries': 3}
merged2 = {**defaults, **overrides}      # equivalent, works in older Python too
Real-world example Merging a user's custom settings with a set of application defaults, letting the user's values take precedence.

Common follow-ups: How would you merge dictionaries where you need CUSTOM logic for handling conflicting keys, rather than simple overwrite?

functools & Functional Programming Tools

Showing 1–10 of 15