python-performance-patterns

smith6jt-cop's avatarfrom smith6jt-cop

Low-risk Python performance optimization patterns with verified speedups

⭐0stars🔀0forks📁View on GitHub🕐Updated Jan 3, 2026⬇️1downloads

When & Why to Use This Skill

This Claude skill provides a curated collection of low-risk, high-impact Python performance optimization patterns designed to accelerate execution speed with minimal refactoring risk. It focuses on practical, verified techniques for common bottlenecks in I/O, data processing, and algorithmic efficiency, helping developers implement safe speedups ranging from 2x to 20x using standard libraries and NumPy.

Use Cases

  • Optimizing data-heavy applications by replacing slow O(n²) nested loops with vectorized NumPy operations for pixel counting or matrix math.
  • Reducing filesystem overhead in batch processing by implementing metadata caching and consolidating multiple directory scans into a single pass.
  • Accelerating I/O-bound tasks such as bulk image loading or file reading using multi-threaded execution patterns.
  • Improving the performance of search and ranking features by switching from full sorts to linear-time selection algorithms for Top-K retrieval.
  • Conducting performance audits to identify and prioritize safe code refactoring opportunities that don't compromise numerical accuracy.
namepython-performance-patterns
description"Low-risk Python performance optimization patterns with verified speedups"
authorKINTSUGI Team
date2025-12-12

Python Performance Patterns

Experiment Overview

Item Details
Date 2025-12-12
Goal Implement safe, high-impact performance optimizations from audit
Environment Python 3.10+, NumPy, scikit-image
Status Success - 5 patterns verified

Context

After a performance audit identified 40+ issues, we needed to prioritize which fixes were safe to implement without extensive testing. These patterns are low-risk, high-reward optimizations.

Pattern 1: @lru_cache for File Metadata

Problem: Functions that check file types open files repeatedly.

Before (3-5 file opens per call):

def get_img_type(img_f):
    is_ome = check_is_ome(img_f)      # Opens file
    can_use_vips = check_to_use_vips(img_f)  # Opens file
    can_use_openslide = check_to_use_openslide(img_f)  # Opens file

After (cached after first call):

from functools import lru_cache

@lru_cache(maxsize=256)
def check_is_ome(src_f):
    # ... implementation

@lru_cache(maxsize=256)
def check_to_use_vips(src_f):
    # ... implementation

@lru_cache(maxsize=256)
def get_img_type(img_f):
    # ... implementation

Speedup: 3-5x for batch file operations

Safe because: File metadata doesn't change during a session


Pattern 2: Vectorized Overlap Matrix

Problem: O(n²) nested loop for pixel-by-pixel counting.

Before:

overlap = np.zeros((max_label1 + 1, max_label2 + 1), dtype=np.int32)
for i in range(labels1.shape[0]):
    for j in range(labels1.shape[1]):
        l1 = labels1[i, j]
        l2 = labels2[i, j]
        overlap[l1, l2] += 1

After:

overlap = np.zeros((max_label1 + 1, max_label2 + 1), dtype=np.int64)
np.add.at(overlap, (labels1.ravel(), labels2.ravel()), 1)

Speedup: 5-10x for large label arrays

Safe because: Mathematically equivalent, uses NumPy built-in


Pattern 3: argpartition for Top-K

Problem: Full sort when only need k smallest/largest values.

Before (O(n log n)):

neighbor_indices = np.argsort(distances[i])[1:n_neighbors+1]

After (O(n)):

neighbor_indices = np.argpartition(distances[i], n_neighbors+1)[1:n_neighbors+1]

Speedup: 3-5x for large arrays

Safe because: Returns same indices, just not sorted (usually fine for k-NN)

Caveat: If you need the k values in sorted order, add a second sort on just k elements.


Pattern 4: Parallel Image Loading

Problem: Sequential I/O when loading many images.

Before:

img_list = [io.imread(os.path.join(src_dir, f)) for f in img_f_list]

After:

from concurrent.futures import ThreadPoolExecutor

img_paths = [os.path.join(src_dir, f) for f in img_f_list]
with ThreadPoolExecutor(max_workers=min(8, len(img_paths))) as executor:
    img_list = list(executor.map(io.imread, img_paths))

Speedup: 10-20x for 50+ images

Safe because: Image reads are independent, thread-safe


Pattern 5: Single Directory Scan

Problem: Multiple glob calls scan directory repeatedly.

Before (5 filesystem scans):

patterns = ["*.tif", "*.tiff", "*.zarr", "*.ome.tif", "*.ome.tiff"]
for pattern in patterns:
    for f in cycle_dir.glob(pattern):
        process(f)

After (1 filesystem scan):

valid_extensions = {".tif", ".tiff", ".zarr"}
valid_suffixes = {".ome.tif", ".ome.tiff"}
for f in cycle_dir.iterdir():
    suffix_lower = f.suffix.lower()
    name_lower = f.name.lower()
    if suffix_lower in valid_extensions or any(name_lower.endswith(s) for s in valid_suffixes):
        process(f)

Speedup: 2-3x for directories with many files

Safe because: Same files matched, just more efficiently


Failed Attempts (Critical)

Attempt Why it Failed Lesson Learned
Caching image processing results Wrong results for different inputs Only cache metadata, not computed results
Caching BaSiC illumination profiles 15-20% intensity errors for sparse markers Each channel needs its own profile (see basic-caching-evaluation skill)
Parallelizing CPU-bound NumPy GIL contention, no speedup ThreadPoolExecutor only for I/O-bound tasks
Removing "unnecessary" array copies Broke downstream code expecting copies Test thoroughly before removing .copy()

Priority Matrix

Priority Pattern Risk Speedup
P0 Parallel image loading Low 10-20x
P1 @lru_cache metadata Low 3-5x
P1 Vectorized overlap Low 5-10x
P2 argpartition Low 3-5x
P2 Single directory scan Low 2-3x
P3 Remove dask copies Medium 2x memory

When NOT to Apply These Patterns

  1. @lru_cache: Don't cache if function has side effects or file might change
  2. argpartition: Don't use if you need results in sorted order
  3. ThreadPoolExecutor: Don't use for CPU-bound operations (use ProcessPoolExecutor)
  4. Removing copies: Don't remove if downstream code modifies the array

Key Insights

  • Start with I/O optimizations - biggest wins with lowest risk
  • Vectorization beats loops in NumPy, always
  • Profile before optimizing - intuition is often wrong
  • Test numerical accuracy after optimization, not just correctness

References

  • NumPy performance tips documentation
  • Python concurrent.futures documentation
  • KINTSUGI Performance Audit (2025-12-12)