Python idioms, ML evaluation metrics, model serving, and data engineering patterns.
A function with no `return` hands back `None`, so a forgotten one surfaces as a `TypeError` at the call site rather than at the line that actually caused it.
`minutes * 60` returns an `int` when given an `int`, while `/` always produces a float regardless of the operands. Multiplication preserves the type; division does not.
`base * height / 2` always yields a float in Python 3 because `/` never returns an int, even for whole numbers. `//` floors instead, which is almost never what an area wants.
Python integers have no fixed width, so `n + 1` never overflows the way a 32-bit `int` would. The value simply grows to whatever memory allows.
`%` gives the remainder and `//` the floor of the division, and `divmod()` returns both in one call. With negatives the result follows the divisor's sign, so `-7 % 3` is `2`.
Unit conversion is one multiply per step: `hours * 3600 + minutes * 60`. Converting each unit separately and adding avoids the rounding drift that chained divisions introduce.
Python chains comparisons, so `0 <= n <= 10` is one expression and evaluates `n` exactly once. Writing it with `and` gives the same answer but evaluates the middle term twice.
Multiplying each count by its legs and summing is `O(1)` no matter how large the herd, using arithmetic rather than iteration. Looping over individual animals does the same work n times over.
`n % 2` gives `0` for even and `1` for odd. Python keeps the sign of the divisor, so `-3 % 2` is `1` here where C and JavaScript would return `-1`.
`lst[0]` raises `IndexError` on an empty list, where `lst[:1]` quietly returns an empty list. Negative indices count from the end, making `lst[-1]` the last element.
`int("12")` parses, but `int("12.5")` raises `ValueError` rather than truncating. `int(12.5)` does truncate, toward zero, which is why the two look inconsistent.
`split()` with no argument collapses runs of whitespace and ignores leading and trailing spaces. `split(" ")` does neither, so a double space yields an empty string in the result.
`min()` and `max()` each walk the list once, so calling both is two passes over the same data. On an empty list they raise `ValueError` unless a `default=` is supplied.
An empty string is falsy, so `if not s:` covers it without calling `len()`. That same test also catches `None`, which is usually what you want and occasionally the bug.
`len()` on a `str` counts characters, not bytes, so an accented letter still counts as 1. It is `O(1)` because Python stores the length rather than walking the string.
An f-string builds one new string in a single pass, evaluating each expression where it appears. Repeated `+` inside a loop allocates a fresh string on every iteration instead.
Recursion mirrors the mathematical definition but stacks a frame per call, hitting Python's default limit near 1000. `math.factorial()` is iterative in C and has no such ceiling.
`in` scans a list front to back, so membership costs `O(n)`. On a `set` the identical expression becomes a hash lookup at `O(1)`: same syntax, very different work.
`[::-1]` returns a reversed copy, `.reverse()` mutates in place and returns `None`, and `reversed()` gives a lazy iterator that copies nothing. Three spellings, three different costs.
`isinstance(x, int)` returns `True` for a `bool`, because `bool` subclasses `int` in Python. Filtering for numbers without excluding booleans quietly keeps `True` and `False` in the result.
`str.translate()` walks the string once against a lookup table, where chained `.replace()` calls walk it once each. The table also cannot re-replace a character an earlier pass produced.
`str.count()` scans the whole string, so counting one character is `O(n)`. Counting several in a loop repeats that scan each time, where `collections.Counter` walks it once for all of them.
`sum(n**3 for n in range(x))` streams one value at a time, where the bracketed comprehension materialises every cube first. Same answer, `O(1)` memory against `O(n)`.
`s[::-1]` builds a reversed copy, so comparing it against `s` is a palindrome test in one expression. It costs `O(n)` extra memory that two pointers would avoid.
An empty dict is falsy, so `if not d:` is the idiomatic check. `d == {}` is true for it as well, but cannot tell an empty dict from `None`: only `is None` does that.
`[x] * n` repeats the reference rather than the object, so `[[]] * 3` produces three names for one list. Mutating one appears to change all three; a comprehension builds distinct objects.
`[n for n in range(x) if n % 2 == 0]` materialises the entire list in memory. Swapping the brackets for parentheses gives a generator that produces each value only when asked.
`s[:i]` copies the first `i` characters, so building every prefix of a string totals `O(n^2)` work. Fine for a word, worth knowing before it meets a large input.
`d["key"]` raises `KeyError` when the key is absent; `d.get("key")` returns `None` instead. Which you want depends on whether a missing key is a bug or an expected case.
One range nests inside another when its `min` is no lower and its `max` no higher. Comparing those two boundaries is a single `O(n)` pass; sorting first is unnecessary.