DEV Community

Timevolt
Timevolt

Posted on

Python Tricks for Competitive Programming: Level Up Like a Jedi

The Quest Begins (The "Why")

I still remember my first Codeforces round. I was cruising through the easy problems, feeling confident, until I hit a question that asked for the number of sub‑arrays with a sum divisible by k. My naive solution was O(n²) — two nested loops, a running sum, and a lot of wasted time. I watched my rating plummet while the leaderboard filled with names I swore were just “lucky”.

That night I told myself: there has to be a smarter way. I dug into Python’s standard library, hoping to find a hidden gem that would let me rewrite the solution in a few lines. What I discovered felt like uncovering a cheat code — simple, elegant, and surprisingly unknown to many competitive programmers.

The Revelation (The Insight)

1. The Walrus Operator (:=) – Inline Assignment

Python 3.8 introduced the assignment expression, affectionately called the walrus operator. At first glance it looks like a syntactic gimmick, but in CP it lets you compute a value and use it in the same expression, saving you from repetitive work and extra variables.

Gotcha: If you forget that the walrus binds tighter than most operators, you can end up with surprising precedence. Wrap the assignment in parentheses when you’re unsure.

Why it matters: In problems where you need to repeatedly read input, compute a value, and test a condition, the walrus cuts down on boilerplate and makes the intent crystal‑clear.

Before (the struggle):

import sys
data = sys.stdin.read().split()
it = iter(data)
t = int(next(it))
out = []
for _ in range(t):
    n = int(next(it))
    arr = [int(next(it)) for _ in range(n)]
    # compute prefix sums manually
    pref = 0
    seen = {0: 1}
    ans = 0
    for x in arr:
        pref += x
        rem = pref % k
        ans += seen.get(rem, 0)
        seen[rem] = seen.get(rem, 0) + 1
    out.append(str(ans))
print("\n".join(out))
Enter fullscreen mode Exit fullscreen mode

After (the victory):

import sys
data = sys.stdin.read().split()
it = iter(data)
t = int(next(it))
out = []
for _ in range(t):
    n = int(next(it))
    k = int(next(it))          # <-- note: we now read k inside the loop
    arr = [int(next(it)) for _ in range(n)]
    pref = 0
    seen = {0: 1}
    ans = 0
    for x in arr:
        pref += x
        if (rem := pref % k) in seen:      # walrus saves the remainder
            ans += seen[rem]
        seen[rem] = seen.get(rem, 0) + 1
    out.append(str(ans))
print("\n".join(out))
Enter fullscreen mode Exit fullscreen mode

The walrus let us compute pref % k once, reuse it for the lookup, and avoid a temporary variable. The code reads like a story: “add the element, get the remainder, if we’ve seen it before add its count, then record it.”

2. itertools.accumulate with an initial Argument – Prefix Sums in One Line

Most CP solutions build prefix sums with a manual loop. itertools.accumulate does the same, but few know that since Python 3.8 it accepts an initial argument, letting you generate a list that starts with a sentinel value (usually 0) without extra steps.

Gotcha: If you omit initial, the first element of the result is the first input value, not zero. Forgetting this leads to off‑by‑one bugs when you later query pref[r] - pref[l-1].

Why it matters: One‑liner prefix sums mean less room for mistake, faster typing, and the ability to chain with other itertools functions (like groupby or compress).

Before (the struggle):

pref = [0]
for v in arr:
    pref.append(pref[-1] + v)
Enter fullscreen mode Exit fullscreen mode

After (the victory):

from itertools import accumulate
pref = list(accumulate(arr, initial=0))
Enter fullscreen mode Exit fullscreen mode

That’s it — no temporary variable, no explicit append. The resulting pref has length n+1 and pref[i] is the sum of the first i elements, exactly what we need for range‑sum queries.

3. bisect – Maintaining a Sorted List with Logarithmic Insert

When a problem asks for order statistics (e.g., “how many numbers seen so far are ≤ x?”) many reach for a binary indexed tree or a segment tree. In Python, a plain list combined with the bisect module gives you O(log n) search and O(n) insertion — which is fine when the total number of insertions is modest (≤ 2·10⁵) and the constant factor of a Fenwick tree would dominate.

Gotcha: bisect.insort inserts in place, shifting elements. If you mistakenly assign its return value (which is None) back to the list, you’ll end up with a None and a runtime error.

Why it matters: The code is dead‑simple, readable, and often faster than implementing a BIT from scratch because it leverages CPython’s highly optimized list internals.

Before (the struggle):

sorted_list = []
for x in stream:
    # manual binary search (error‑prone)
    lo, hi = 0, len(sorted_list)
    while lo < hi:
        mid = (lo + hi) // 2
        if sorted_list[mid] < x:
            lo = mid + 1
        else:
            hi = mid
    sorted_list.insert(lo, x)
    # now answer queries using sorted_list
Enter fullscreen mode Exit fullscreen mode

After (the victory):

from bisect import bisect_left, insort
sorted_list = []
for x in stream:
    idx = bisect_left(sorted_list, x)   # O(log n)
    # do something with idx, e.g., count of smaller elements
    insort(sorted_list, x)              # O(n) shift, but clean
Enter fullscreen mode Exit fullscreen mode

Notice how we separated the search (bisect_left) from the insertion (insort). This makes the intent explicit and avoids the common bug of forgetting that insort returns None.

Why This New Power Matters

Mastering these three tricks does more than shave a few milliseconds off your runtime — it changes how you think about problems.

  • The walrus operator teaches you to express intent locally, reducing mental clutter when you read your own code weeks later.
  • accumulate with initial reminds you that Python’s standard library already contains many of the building blocks you reach for in C++ or Java; you just need to know where to look.
  • bisect shows that sometimes the simplest data structure (a sorted list) is enough, and you don’t always need to over‑engineer a solution.

When you internalize these patterns, you start spotting opportunities to replace noisy loops with expressive one‑liners, and you spend less time debugging off‑by‑one errors. In a contest, that translates to more problems solved, fewer panic‑induced bugs, and a calmer mind when the clock is ticking.

The Challenge Ahead

Here’s a mini‑quest for you: take the classic “maximum subarray sum modulo m” problem. Try solving it once with a manual prefix‑sum loop and a dictionary, and once using walrus, accumulate, and bisect together. Compare the length, readability, and speed of your two versions.

Drop your results in the comments, and let’s see who can write the most elegant solution. May the force be with you — and may your code be as tight as a lightsaber’s hilt. Happy hacking!

Top comments (0)