10 Python Best Practices Every Developer Should Know

Why Your “Clean” Code Killed Our Production Node

# The "Clean" HFT Signal Processor - Version 1.0.4-CRASH
import json
import time

# Global state because "it's easier to access across modules"
GLOBAL_TICK_DATA = []
LAST_PROCESSED_PRICE = 0.0

def process_ticks(file_path):
    # Manual file handling because context managers are "too much nesting"
    f = open(file_path, 'r')
    data = f.read()

    # Splitting the whole file into memory at once
    lines = data.split('\n')

    for line in lines:
        if line == "":
            continue

        # Parsing JSON inside a hot loop
        tick = json.loads(line)
        GLOBAL_TICK_DATA.append(tick)

        # Nested O(n) lookup to "validate" history
        is_duplicate = False
        for entry in GLOBAL_TICK_DATA[:-1]:
            if entry['id'] == tick['id']:
                is_duplicate = True
                break

        if not is_duplicate:
            calculate_signal(tick)

    f.close()

def calculate_signal(tick):
    global LAST_PROCESSED_PRICE
    # Using global variables for state tracking
    price_diff = tick['price'] - LAST_PROCESSED_PRICE
    if price_diff > 0.001:
        execute_trade(tick['id'], "BUY")
    LAST_PROCESSED_PRICE = tick['price']

def execute_trade(order_id, side):
    # Simulating an API call with a blocking sleep
    print(f"Executing {side} for {order_id}")
    time.sleep(0.01)

process_ticks("market_data_heavy.json")

I. The Heap Fragmentation Nightmare

I’ve spent twenty years optimizing assembly for x86 and ARM. I’ve written drivers where every bit in a register was a precious resource. Then I walk into the office on a Tuesday morning and find our primary HFT node—a machine with 256GB of RAM and 64 cores—choked to death. The OOM (Out of Memory) killer didn’t just stop the process; it dragged the kernel into a swap-death spiral because of the garbage you see above.

Look at GLOBAL_TICK_DATA.append(tick). In your mind, you’re just adding an item to a list. In the reality of Python 3.12.2, you are committing a crime against the memory controller. A Python list is not a contiguous array of structures like a C++ std::vector<Tick>. It is a contiguous array of pointers to PyObject structures.

Each tick dictionary you’re appending is a heap-allocated object. Each key and value in that dictionary is another heap-allocated object. When you append to that list, Python occasionally has to realloc the underlying pointer array. Because you’re doing this in a loop with millions of ticks, you are forcing the allocator to constantly find new, larger contiguous blocks of memory for the pointer array, while the actual data—the dictionaries—is scattered across the heap like buckshot. This is the definition of cache thrashing. The CPU’s L1 and L2 caches are useless here. Every time you access GLOBAL_TICK_DATA, the CPU is waiting hundreds of cycles for a main memory fetch because the pointer points to a random address in the 256GB abyss.

II. The Global Lookup Tax and Bytecode Inefficiency

You used global LAST_PROCESSED_PRICE. You probably think that’s a “python best” practice for maintaining state in a small script. It isn’t. It’s a performance suicide note.

In Python 3.12.2, local variables are stored in an array on the stack frame. Accessing them is a simple LOAD_FAST opcode—essentially an array index lookup. But LAST_PROCESSED_PRICE is a global. To find it, the interpreter must execute LOAD_GLOBAL. This triggers a multi-step search: first, it checks the __dict__ of the current module. If it’s not there, it checks the built-ins. Even with the “specialized adaptive interpreter” improvements in 3.12, you are still performing a hash table lookup where a simple stack offset would suffice.

Let’s look at the cProfile output from the crash:

python3 -m cProfile -s time crash_repro.py
         15000005 function calls in 124.5 seconds

   Ordered by: internal time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1   82.100   82.100  124.500  124.500 crash_repro.py:10(process_ticks)
  5000000   22.400    0.000   22.400    0.000 {built-in method json.loads}
  5000000   15.000    0.000   15.000    0.000 crash_repro.py:38(calculate_signal)
  5000000    5.000    0.000    5.000    0.000 {method 'append' of 'list' objects}

Eighty-two seconds spent in process_ticks. Look at the pip freeze for this environment:

jsonlines==4.0.0
numpy==1.26.4
pandas==2.2.1
python-dateutil==2.9.0
six==1.16.0

You have numpy installed, yet you’re using a raw Python list to store tick data. It’s like owning a Ferrari and choosing to push it to work.

III. The O(n^2) Complexity Trap

The most offensive part of this code is the “duplicate check”:

is_duplicate = False
for entry in GLOBAL_TICK_DATA[:-1]:
    if entry['id'] == tick['id']:
        is_duplicate = True
        break

This is a textbook O(n^2) disaster. For every new tick, you iterate through the entire history of ticks. When the market is moving fast and we receive 1,000,000 ticks, the 1,000,001st tick requires 1,000,000 comparisons. By the time you’re at 5,000,000 ticks, you’re doing trillions of operations.

In a C++ environment, we would use a std::unordered_set or a bloom filter if we could tolerate false positives. In Python, you should have used a set. But instead, you chose to burn the CPU to the ground. This isn’t just “slow” code; it’s mathematically incompetent. You are literally waiting for the heat death of the universe while the market moves on without us.

IV. Manual File Handling and the Ghost of File Descriptors

You didn’t use a context manager. f = open(file_path, 'r') followed by a manual f.close(). If json.loads(line) raises a ValueError—which it will, because market data is notoriously filthy—the f.close() line is never reached.

In a long-running trading node, this leads to file descriptor leaks. Eventually, the process hits the ulimit for open files and crashes. But that’s not even the worst part. You used f.read(). You read the entire multi-gigabyte file into a single string object.

Python strings are UTF-8 (or UCS-4 depending on the content). A 1GB file doesn’t just take 1GB of RAM; it takes significantly more due to the PyObject overhead and the way Python handles string internals. You are forcing the OS to evict useful page caches just to hold a massive string that you’re immediately going to split into another massive list of strings. You’re doubling your memory pressure for no reason other than laziness.

V. Bytecode Analysis of the Global Lookup

Let’s look at what the Python 3.12 interpreter is actually doing with your calculate_signal function. If we run dis.dis(calculate_signal), we see the horror:

  0 RESUME                   0
  2 LOAD_GLOBAL              1 (LAST_PROCESSED_PRICE)
 12 LOAD_FAST                0 (tick)
 14 LOAD_CONST               1 ('price')
 16 BINARY_SUBSCR
 26 BINARY_OP                10 (-)
 30 LOAD_CONST               2 (0.001)
 32 COMPARE_OP              68 (>)
 ...
 50 STORE_GLOBAL             1 (LAST_PROCESSED_PRICE)

The LOAD_GLOBAL at offset 2 and STORE_GLOBAL at offset 50 are the killers. In 3.12, there is an “inline cache” for LOAD_GLOBAL, but it still requires a check to see if the globals dictionary has changed (the “version tag”). In a high-frequency loop, these nanoseconds aggregate into milliseconds. Milliseconds in HFT are the difference between a profit and a liquidation.

Furthermore, BINARY_SUBSCR for tick['price'] is another dictionary lookup. Every single time you access a field in that dictionary, you are hashing the string ‘price’, looking it up in the dictionary’s hash table, and retrieving the object. This is “magic” at its worst. You’re doing work that should have been resolved at compile time.

VI. The GIL and the Illusion of Concurrency

I saw your original draft of this code before the crash. You tried to “fix” the speed by wrapping process_ticks in a threading.Thread. This is where I almost resigned.

Python’s Global Interpreter Lock (GIL) ensures that only one thread executes Python bytecode at a time. By adding threads to a CPU-bound task like parsing JSON and iterating through lists, you didn’t make it faster. You made it slower. The overhead of the OS context switching between threads, combined with the GIL contention, means the CPU spends more time managing the threads than actually processing the data.

If you wanted concurrency, you should have used multiprocessing to bypass the GIL, but even then, the cost of serializing and deserializing data (Pickling) across process boundaries would likely outweigh the benefits for this specific workload. You’re trying to use high-level abstractions to solve a low-level data throughput problem. It’s like trying to put a spoiler on a tractor to make it win a Formula 1 race.

VII. The Rebuild: How a Professional Handles Data

If we want this node to stay alive, we have to stop treating Python like a playground and start treating it like a wrapper for C. We use numpy for contiguous memory. We use slots to avoid dictionary overhead. We use generators to keep the memory footprint flat.

Here is how you write this if you actually care about the firm’s capital.

import numpy as np
import json
from typing import Final

# Use a class with slots to prevent __dict__ creation
class TickProcessor:
    __slots__ = ('last_price', 'seen_ids')

    def __init__(self):
        self.last_price: float = 0.0
        # A set uses a hash table for O(1) average lookup
        self.seen_ids: set[int] = set()

    def process_stream(self, file_path: str):
        # Use a context manager and a generator to keep memory flat
        try:
            with open(file_path, 'r', buffering=1<<16) as f:
                for line in f:
                    if not line.strip():
                        continue

                    try:
                        # In a real HFT scenario, we'd use ujson or orjson
                        # for faster parsing than the standard library
                        tick = json.loads(line)
                    except json.JSONDecodeError:
                        continue

                    t_id = tick['id']
                    if t_id not in self.seen_ids:
                        self.seen_ids.add(t_id)

                        price = tick['price']
                        if (price - self.last_price) > 0.001:
                            self.execute_trade(t_id, "BUY")
                        self.last_price = price
        except IOError as e:
            print(f"Hardware/IO Error: {e}")

    def execute_trade(self, order_id: int, side: str):
        # In production, this would be a non-blocking socket write
        # to a binary FIX engine, not a print statement.
        pass

# Optimized entry point
if __name__ == "__main__":
    processor = TickProcessor()
    processor.process_stream("market_data_heavy.json")

Why this doesn’t crash the node:

  1. Memory Efficiency: By iterating over the file line-by-line (for line in f), we only ever have one line of data in memory at a time. The memory footprint stays constant regardless of whether the input file is 1MB or 1TB.
  2. O(1) Lookups: Replacing the list scan with a set for seen_ids turns an O(n^2) nightmare into an O(n) linear process. The time to process a tick no longer depends on how many ticks came before it.
  3. Local Variable Optimization: By putting the logic inside a class method, self.last_price and self.seen_ids are accessed via LOAD_FAST (after the initial self lookup), which is significantly faster than LOAD_GLOBAL.
  4. Buffering: I added a 64KB buffer to the file open() call. This reduces the number of system calls the kernel has to make, allowing for much higher throughput from the NVMe drives.
  5. Type Safety (Hints): While Python doesn’t enforce them at runtime, 3.12 uses these hints for internal optimizations in the specializing interpreter. It helps the JIT-like features of the new interpreter make better assumptions about the code.

You see, the “python best” way to write code isn’t to make it look “clean” or “minimalist” according to some blog post written by a web developer. The best way is to understand the underlying CPython implementation. You have to know that a list is an array of pointers. You have to know that json.loads is a heavy operation. You have to know that the GIL is always watching, waiting to punish your poor architectural choices.

If I see another global variable in a performance-critical path, I’m going to personally revoke your access to the production cluster and make you write COBOL until you learn the value of a well-managed heap.

Now, go fix the rest of the ingestion pipeline. And for the love of God, stop using pandas for single-row insertions.

# Terminal Log: Post-Optimization Run
$ python3 -m cProfile -s time optimized_processor.py
         5000005 function calls in 4.2 seconds

   Ordered by: internal time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    2.100    2.100    4.200    4.200 optimized_processor.py:12(process_stream)
  5000000    1.500    0.000    1.500    0.000 {built-in method json.loads}
  5000000    0.600    0.000    0.600    0.000 {method 'add' of 'set' objects}

4.2 seconds. Down from 124.5. That is the difference between a job and a pink slip.

$ _

Leave a Comment