Python
list manipulation
algorithms
data processing
programming tutorials

Splitting a list into N parts of approximately equal length

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Splitting a list into roughly equal parts is a small problem that appears in a lot of real work. It comes up when dividing data for batch jobs, distributing items across workers, paginating results, or chunking input for APIs. The goal is usually not random splitting; it is predictable partitioning where chunk sizes differ by at most one item.

The Core Rule

If a list has m items and you want n chunks, the cleanest rule is:

  • every chunk gets a base size of m // n
  • the first m % n chunks get one extra item

That guarantees two properties:

  1. Every element is used exactly once.
  2. No two chunk sizes differ by more than one.

Python's divmod() expresses this neatly.

A Reliable Implementation

Here is a straightforward implementation that returns a list of lists:

python
1def split_evenly(items, n):
2    if n <= 0:
3        raise ValueError("n must be greater than 0")
4
5    size, remainder = divmod(len(items), n)
6    result = []
7    start = 0
8
9    for index in range(n):
10        stop = start + size + (1 if index < remainder else 0)
11        result.append(items[start:stop])
12        start = stop
13
14    return result
15
16
17values = list(range(10))
18parts = split_evenly(values, 3)
19print(parts)

Output:

text
[[0, 1, 2, 3], [4, 5, 6], [7, 8, 9]]

The first chunk gets the extra element because the remainder is 1. If the remainder were 2, the first two chunks would be one item longer.

Why This Beats Naive Slicing

A common first attempt is to compute chunk_size = len(items) // n and slice repeatedly using that number. The problem is that integer division drops the remainder, so some elements are lost unless you add special handling at the end.

Another weak approach is using floating-point boundaries and rounding them. That can work, but it is harder to reason about and easier to get subtly wrong when indexes repeat or skip.

The divmod() approach is better because it is exact and easy to audit in code review.

Handling More Chunks Than Items

If n is greater than the number of elements, some chunks must be empty. That is not a bug. It is the only mathematically correct result if you insist on exactly n partitions.

python
print(split_evenly([10, 20, 30], 5))

Output:

text
[[10], [20], [30], [], []]

Whether empty chunks are acceptable depends on the use case. For worker distribution, they are often fine because idle workers simply do nothing. For user-facing pagination, you may prefer to return at most len(items) chunks instead.

Returning a Generator Instead of a Full List

If the input is large and you want to stream chunks one by one, return an iterator. The splitting logic is the same.

python
1def iter_splits(items, n):
2    if n <= 0:
3        raise ValueError("n must be greater than 0")
4
5    size, remainder = divmod(len(items), n)
6    start = 0
7
8    for index in range(n):
9        stop = start + size + (1 if index < remainder else 0)
10        yield items[start:stop]
11        start = stop
12
13
14for part in iter_splits(list("abcdefghij"), 4):
15    print(part)

This is useful when each chunk is sent to a separate subsystem or processed immediately, because you do not need to materialize the outer list first.

Preserving Order Versus Balancing Work

The examples above preserve the original order of the input. That is usually what you want for deterministic processing. But keep in mind that equal item counts do not always mean equal work.

Suppose each list item represents a file and file sizes vary widely. Splitting by count may still produce very uneven processing time. In that situation, the real problem is workload balancing, not list partitioning. You may need weighted assignment or a queue-based worker pool instead.

That distinction matters because many developers try to solve a scheduling problem with a slicing function.

Practical Uses

This pattern is especially useful in three cases:

  • batch API calls where each chunk becomes one request
  • test-data partitioning across workers in CI
  • dividing a sequence into training, validation, and holdout segments when exact boundaries matter

The function remains simple because it does one thing well: partition by count while preserving order.

Common Pitfalls

The first pitfall is forgetting to validate n. A value of zero should fail fast, not trigger a divide-by-zero error later.

The second is assuming every chunk must be non-empty. That only holds when n is less than or equal to the number of elements.

Another issue is confusing approximate equality with randomization. This algorithm does not shuffle. If you need randomized groups, shuffle the list first and then split it.

Finally, be careful with mutable nested objects. Slicing creates a new outer list, but the inner objects are still shared references. If you mutate a dictionary inside one chunk, the original object changes too.

Summary

  • Use divmod(len(items), n) to compute balanced chunk sizes exactly.
  • The first remainder chunks should receive one extra item.
  • This guarantees chunk sizes differ by at most one element.
  • Empty chunks are expected when n exceeds the item count.
  • Prefer a generator form when processing large inputs incrementally.
  • Equal chunk sizes do not guarantee equal processing time for uneven workloads.

Course illustration
Course illustration

All Rights Reserved.