Python habits I picked up after wasting months on stack traces

Most people learning Python copy the first pattern they find on Stack Overflow and never think about it again. I spent about four years in data infrastructure before I started caring about things like memory overhead in list comprehensions and why my JSON parsing scripts were choking on files under 200 megabytes. The fixes were usually trivial once you knew where to look. Here's the kind of Step By Step Guide For Python Tips And Tricks I wish I had when I was still writing code that looked fine locally but fell apart under load.

Use the walrus operator to collapse two lines into one

Before Python 3.8, if you wanted to match a regex and then use the result, you had to do it in two steps. Something like matching a timestamp at the start of every log line. With the walrus operator, you can do this in a single conditional: The gain isn't huge, but it removes a variable from the outer scope and makes the intent clearer. I use this pattern constantly in CLI tools where you're reading lines from stdin and checking for patterns on the fly.

setdefault() is convenient but it creates a new list object on every call, even when the key already exists. That sounds minor until you're looping over a million rows. Use collections.defaultdict instead. It uses the C-level dict implementation under the hood and only constructs the default value when the key is actually missing: I ran into this specifically when processing CSV exports from a legacy reporting system. Switching from setdefault to defaultdict cut my processing time from about 45 minutes down to roughly 8 on the same machine. The difference came from the repeated list allocation in the inner loop.

Python 3.8 added debug formatting to f-strings. Instead of writing print(f"value is {x}"), you just write print(f"{x=}"). It outputs x=42 with the variable name included. Useful during development when you're tracing through a function and don't want to maintain separate print statements. It also works inside expressions:

result = f"processed {data=}"

This is harmless but it does add a tiny overhead because the expression is being evaluated twice — once for the value and once for the name. Don't use it in tight loops or performance-critical paths. By default, every Python object gets a __dict__ attribute that stores its instance variables as a dictionary. This is flexible but it uses a lot of memory. Each instance of a plain class with five attributes might take around 200 bytes on a 64-bit CPython build. Adding __slots__ replaces that dictionary with fixed offsets:

Get the Full Details

HD wallpaper: sunset, beach, niterói, eventide, mar, ceu, holidays, by ...
HD wallpaper: sunset, beach, niterói, eventide, mar, ceu, holidays, by ...
class Point:
    __slots__ = ('x', 'y', 'z')
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z

That drops per-instance memory to roughly 64 bytes. If you're storing millions of these in a spatial index or a graph structure, it matters. I hit this when building a point-cloud processing pipeline. The memory usage went from about 1.2 GB down to under 400 MB just by adding slots. The catch is that __slots__ disables dynamic attribute assignment and makes inheritance slightly awkward. If you need to add attributes at runtime or subclass extensively, skip it.

Use itertools.groupby but remember it requires sorted input

groupby is often recommended for grouping data, but most people use it wrong. It does not sort your data for you. It groups consecutive identical keys only. If your data isn't already sorted by the grouping key, you'll get multiple groups for the same key value and your output will be wrong. I learned this the hard way when I was merging transaction logs from two sources. The combined dataset wasn't sorted, so groupby produced fragmented output and I spent an hour debugging what looked like duplicate categories in my report. Sorting first fixed it instantly. Lists of strings that need uppercasing:

A lambda creates a Python function object on every iteration. map() with a built-in bypasses that overhead because the C implementation calls the built-in directly. The difference is small on short lists but measurable on larger datasets. I timed this on a list of about 500,000 strings. The lambda version took roughly 0.8 seconds. The built-in map took about 0.3 seconds. It's not a performance win. It's a correctness win. Path concatenation with string formatting breaks on Windows, and it gets messy fast when you're dealing with relative paths and parent directories. This handles OS-specific separators, resolves .. components, and works consistently across platforms. I used to write path handling code with os.path.join and ended up with broken paths every time someone tested on a different operating system. Switched to pathlib three years ago and haven't looked back.

A bare except block that does nothing is a code smell. But when you genuinely need to ignore a specific exception, contextlib.suppress is cleaner: Compared to a try-except block, this removes several lines and makes the intent explicit. The file not existing isn't an error condition in this context — it's expected. Suppress handles that. Caching pure function results is one of the easiest speedups you can apply. But @lru_cache has limits. By default, it stores up to 128 entries. If your function is called with many different argument combinations, the cache grows and memory usage climbs. You can set maxsize=None to remove the limit, but then you're trading memory for speed and the cache never evicts anything.

I used it on a function that computed weighted averages over product dimensions. The cache hit rate was high because the same dimension combinations appeared repeatedly. That cut an operation that took about 30 seconds per batch down to under 2 seconds. The function was pure — same inputs always returned the same output — which is the main requirement for safe caching. If your function has side effects or depends on external state, caching silently produces wrong results. I've seen this happen when people cached database query helpers without realizing the underlying data changed between calls.

HD wallpaper: sunset, beach, niterói, eventide, mar, ceu, holidays, by ...
HD wallpaper: sunset, beach, niterói, eventide, mar, ceu, holidays, by ...

Know when to reach for numpy and when not to

NumPy arrays are fast for numerical operations because the computation happens in compiled C code. But creating a NumPy array from a Python list has overhead. If you're doing a single addition on a list of ten numbers, NumPy is slower than native Python because of the conversion cost. The crossover point is usually around a few thousand elements. Below that, plain Python is competitive. Above that, NumPy wins decisively. I wrote a script that computed Euclidean distances between all pairs of points in a dataset. Starting with a nested Python loop on lists, it took about 12 minutes for 10,000 points. Swapping to NumPy with vectorized operations brought it down to roughly 4 seconds. Don't use NumPy for string manipulation, JSON parsing, or general data wrangling. Pandas has its own overhead and isn't a drop-in replacement for lists in every case. Use the right tool for the shape of the data you're working with.

Use generator expressions instead of list comprehensions when you don't need the full list

A list comprehension builds the entire list in memory. A generator expression yields one item at a time: If you're passing this to sum(), max(), or a loop, the generator uses a fraction of the memory. I processed a log file with about 20 million lines and built a list of matching entries using a comprehension. The script used roughly 3.5 GB of RAM and took forever. Switching to a generator expression dropped memory to under 200 MB and the script finished in a reasonable time. The trade-off is that you can't index into a generator or call len() on it. If you need random access later, you still need a list.

Use enumerate() instead of range(len())

This is one of those things that seems obvious in hindsight but I saw people doing wrong for years: The enumerate version is more readable and marginally faster because it avoids the indexing operation on every iteration. On a list of a million items, the difference was about 0.15 seconds in my testing. Not dramatic, but it adds up in tight loops. == checks value equality. is checks identity — whether two names point to the same object in memory. Most people learn this and move on, but there's a detail that trips people up: CPython caches small integers and short strings. So a = 256; b = 256; a is b is True, but a = 257; b = 257; a is b is typically False when written in separate statements.

This isn't guaranteed by the language spec. It's an implementation detail of CPython. Other Python implementations like PyPy or Jython don't do this. If you're writing code meant to run across implementations, never rely on is for value comparison. Use == unless you specifically need to check identity, like comparing against None. Before Python 3.7, creating a simple data container meant writing an __init__ with repeated assignments and a custom __repr__. With dataclasses, the boilerplate disappears: The decorator generates __init__, __repr__, and __eq__ automatically. You can also add defaults, type hints, and frozen instances (immutable objects) with a single parameter. I replaced a bunch of namedtuple classes with dataclasses in a financial reporting tool. The code became shorter and easier to read without losing any functionality.

One thing to watch: frozen dataclasses create a new object on every mutation attempt instead of raising a clear error. If you need explicit mutation control, consider a regular class with properties instead.

HD wallpaper: sunset, beach, niterói, eventide, mar, ceu, holidays, by ...
HD wallpaper: sunset, beach, niterói, eventide, mar, ceu, holidays, by ...