Python Module 19 – Python for coding interviews and online assessments
Python for coding rounds: input, output, limits
Read a million numbers fast with sys.stdin.buffer, write all answers in one call, avoid the recursion and digit limits, and start from one template.
What you will learn
- Explain how a judge runs and checks a program, and keep standard output to the answers alone
- Set up a template that reads the whole input at once, solves, and writes every answer in one call
- Choose when to flush output, for ordinary and for interactive problems
- Convert a deep recursive search into a loop with an explicit stack
- Avoid the integer digit limit and features newer than the judge's Python
Before you start
What you’ll learn · Before you start
What you will learn
- Explain how a judge runs and checks a program, and keep standard output to the answers alone
- Set up a template that reads the whole input at once, solves, and writes every answer in one call
- Choose when to flush output, for ordinary and for interactive problems
- Convert a deep recursive search into a loop with an explicit stack
- Avoid the integer digit limit and features newer than the judge's Python
Before you start
On this page
A coding round runs your program on an online judge: A service that runs a submitted program on hidden tests: it gives each test's input on standard input, compares everything the program prints with the expected answer, and gives a verdict for every test, under a time limit and a memory limit.: each test’s input arrives on
standard streams: The three text channels every program starts with: standard input to read from, standard output for its results and standard error for messages. In Python they are sys.stdin, sys.stdout and sys.stderr. More in “Input and output: print(), input() and stdin”, everything the program prints is compared with the expected answer,
and the run must finish within a time limit, often one or two seconds, and a memory limit. Python is fast enough for
these limits when a solution keeps four habits: read the whole input in one call with sys.stdin.buffer.read().split(),
collect the answers and write them in one call, replace recursion with a loop when the depth grows with the input, and
remember that an int never overflows but refuses to become more than 4,300 decimal digits of text.
Each habit is measured or shown below on CPython 3.14.8, with the time it saves or the error it prevents. You will step through one test of a judge and the template’s walk over its input, and leave with a template to start every solution from.
A judge compares everything you print
Each test runs your program the same way, from its input to a verdict:
What a judge does with your program on every test
Text description of the diagram
The diagram shows one test of an online judge, from top to bottom.
- At the top is the test input, a file that only the judge sees.
- An arrow labelled standard input leads to the box python3 solution.py, which runs within the time limit and the memory limit of the problem.
- An arrow labelled standard output leads from the program to everything it printed.
- Two arrows labelled compared lead from the printed output and from the expected answer to the verdict for this test: accepted, wrong answer, time limit exceeded or runtime error.
A solution is accepted only when every test gets the verdict accepted, so the slowest and deepest test decides.
Nobody types anything and nobody reads a prompt. Two consequences catch people out in their first round:
- A prompt is part of your answer.
input("Enter n: ")writesEnter n:to standard output, so the judge sees it in front of your first answer and reports a wrong answer. Callinput()without a prompt, or better, read the input the way the next section does. - The slowest test decides. A solution is accepted only if every test passes, and the last tests are usually the largest the problem allows. Read the limits in the statement, such as “n ≤ 2 × 10⁵”, as the size your program must handle, not the size of the examples.
Each judge states how strictly it compares, and many ignore spaces at the ends of lines and the final line break.
An extra word or a stray debugging line is a wrong answer everywhere, so debugging output belongs on standard error
(print(..., file=sys.stderr)), which judges do not compare with the answer. Follow one test of a program that still
asks for its input with a prompt, and watch where each line it prints ends up:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- The test starts. The judge starts python3 solution.py with the input of test 1 already waiting on its standard input. Nobody types, and nothing announces that a test has begun.
- A prompt is printed. The program calls input("Enter n: "). input() writes its prompt to standard output, which is exactly where the judge collects the answer.
- The answers follow. The program reads the rest of the input and prints its three answers, 2, 0 and 1, one per line, the right numbers for this test.
- A debugging line. print(total, file=sys.stderr) writes to standard error instead. The judge keeps that stream apart, so the line never reaches the comparison.
- The comparison. The judge compares the output with the expected answer line by line. Its first line is "Enter n: 2" where 2 is expected, so the verdict for test 1 is wrong answer, although every number is right.
- Without the prompt. Called as input(), with no prompt, it writes nothing. The output is then 2, 0 and 1 alone, it matches line for line, and test 1 is accepted.
- Every test must pass. The judge runs the same steps for every test, and the last ones are usually the largest the statement allows. One test over the time limit fails the whole submission.
Every number was right and the verdict was still wrong, because the judge compares the text on standard output and nothing else. Removing the prompt fixed the test without touching the logic.
Checkpoint
Check: How a judge runs and checks a program
One template: read once, solve, write once
The template keeps reading and writing in one place and the thinking in another. The example problem: each class lists its marks, and the answer is how many marks are above the class average.
import sys
def solve(tokens):
"""Answers every test case: how many marks are above the class average."""
it = iter(tokens)
out = []
for _ in range(int(next(it))):
n = int(next(it))
marks = [int(next(it)) for _ in range(n)]
total = sum(marks)
# m > total / n, compared without floats: m * n > total
out.append(str(sum(1 for m in marks if m * n > total)))
return out
def main():
tokens = sys.stdin.buffer.read().split()
sys.stdout.write("\n".join(solve(tokens)) + "\n")
if __name__ == "__main__":
main() Input (standard input)
3 5 40 75 90 60 35 4 50 50 50 50 3 10 20 31
Output
2 0 1
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 template.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
main()reads every byte of standard input in one call and splits it on any whitespace, so it does not matter how the numbers are spread over lines. The third class has its last mark on a line of its own, and nothing breaks.solve()walks through the tokens with an iterator:next(it)takes the next token wherever it is. It returns the answers as a list of strings instead of printing them, which makes it easy to test: a test importssolveand compares its result, as this lesson’s exercise does.main()writes all the answers with onesys.stdout.write(). Theif __name__ == "__main__":guard, the main guard: The block under if __name__ == "__main__": in a file. It runs when the file is started as the program and is skipped when the file is imported, because __name__ then holds the module's own name. More in “Modules, imports and __name__”, runs it only when the file is run as a program, not when a test imports it. Keeping the work inside functions also makes it faster, because Python reaches a function’s local variables more quickly than names at module level; the next lesson measures the difference.- The comparison
m * n > totalasks “is m above the average?” without dividing. Whole numbers stay exact at any size, while a float average such as 61 ÷ 3 is rounded to about 16 significant digits, which can flip a comparison once the numbers are huge.
Step through the same walk over the same input, written out at the top level with a loop for each mark, so that every
token the iterator hands over is a step of its own. The flag under tokens is the token the next next(it) hands
over:
Program · token_walk.py
data = (b"3\n5\n40 75 90 60 35\n"
b"4\n50 50 50 50\n"
b"3\n10 20\n31\n")
tokens = data.split()
it = iter(tokens)
out = []
for _ in range(int(next(it))):
n = int(next(it))
marks = [0] * n
for i in range(n):
marks[i] = int(next(it))
total = sum(marks)
above = sum(1 for m in marks if m * n > total)
out.append(str(above))
print("\n".join(out)) Variables
- just taken by the loop
- changed in this step
- passed by the loop
- read next
- where the loop stopped
- the stop value, never given
Printed so far
Nothing printed yet2 0 1
Every step was recorded from a run with Python 3.14.8 on macOS 26 arm64.
The drawing shows the variables when the program ends, and the program's 52 steps are listed below.
Change the program and run it here. Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Every step as text
- Start. No line has run yet, so there are no variables. Python starts with line 1.
- Line 1.
data = (b"3\n5\n40 75 90 60 35\n" b"4\n50 50 50 50\n" b"3\n10 20\n31\n")setsdatatob'3\n5\n40 75 90 60 35\n4\n50 50 50 50\n3\n10 20\n31\n'. - Line 4.
tokens = data.split()setstokensto[b'3', b'5', b'40', b'75', b'90', b'60', b'35', b'4', …]. split() with no argument splits on any run of spaces and line breaks, so the eight lines of input become one flat list of 16 tokens, each of them bytes. - Line 5.
it = iter(tokens)setsitto<list_iterator object>. Key point it is an iterator over tokens. Each next(it) hands over the token under the flag and moves the flag one place on, so no line of the program counts positions. - Line 6.
out = []makesouta list of 0 items. - Line 7.
for _ in range(int(next(it))):gives its first number:_is0. - Line 8.
n = int(next(it))setsnto5. The 5 says how many marks the first class has. The marks are simply the next five tokens, wherever their lines end. - Line 9.
marks = [0] * nsetsmarksto[0, 0, 0, 0, 0]. - Line 10.
for i in range(n):gives its first number:iis0. - Line 11.
marks[i] = int(next(it))changesmarks: it is now[40, 0, 0, 0, 0]. - Line 10.
for i in range(n):gives the next number:iis1. - Line 11.
marks[i] = int(next(it))changesmarks: it is now[40, 75, 0, 0, 0]. - Line 10.
for i in range(n):gives the next number:iis2. - Line 11.
marks[i] = int(next(it))changesmarks: it is now[40, 75, 90, 0, 0]. - Line 10.
for i in range(n):gives the next number:iis3. - Line 11.
marks[i] = int(next(it))changesmarks: it is now[40, 75, 90, 60, 0]. - Line 10.
for i in range(n):gives the next number:iis4. - Line 11.
marks[i] = int(next(it))changesmarks: it is now[40, 75, 90, 60, 35]. - Line 10.
for i in range(n):has no number left: a range stops before its stop value,5, so the loop ends. - Line 12.
total = sum(marks)setstotalto300. - Line 13.
above = sum(1 for m in marks if m * n > total)setsaboveto2. - Line 14.
out.append(str(above))adds'2'to the end ofout. - Line 7.
for _ in range(int(next(it))):gives the next number:_is1. - Line 8.
n = int(next(it))setsnto4(it was5). - Line 9.
marks = [0] * nsetsmarksto[0, 0, 0, 0](it was[40, 75, 90, 60, 35]). - Line 10.
for i in range(n):gives its first number:iis0. - Line 11.
marks[i] = int(next(it))changesmarks: it is now[50, 0, 0, 0]. - Line 10.
for i in range(n):gives the next number:iis1. - Line 11.
marks[i] = int(next(it))changesmarks: it is now[50, 50, 0, 0]. - Line 10.
for i in range(n):gives the next number:iis2. - Line 11.
marks[i] = int(next(it))changesmarks: it is now[50, 50, 50, 0]. - Line 10.
for i in range(n):gives the next number:iis3. - Line 11.
marks[i] = int(next(it))changesmarks: it is now[50, 50, 50, 50]. - Line 10.
for i in range(n):has no number left: a range stops before its stop value,4, so the loop ends. - Line 12.
total = sum(marks)setstotalto200(it was300). - Line 13.
above = sum(1 for m in marks if m * n > total)setsaboveto0(it was2). - Line 14.
out.append(str(above))adds'0'to the end ofout. - Line 7.
for _ in range(int(next(it))):gives the next number:_is2. - Line 8.
n = int(next(it))setsnto3(it was4). - Line 9.
marks = [0] * nsetsmarksto[0, 0, 0](it was[50, 50, 50, 50]). - Line 10.
for i in range(n):gives its first number:iis0. - Line 11.
marks[i] = int(next(it))changesmarks: it is now[10, 0, 0]. - Line 10.
for i in range(n):gives the next number:iis1. - Line 11.
marks[i] = int(next(it))changesmarks: it is now[10, 20, 0]. - Line 10.
for i in range(n):gives the next number:iis2. - Line 11.
marks[i] = int(next(it))changesmarks: it is now[10, 20, 31]. 31 stood on a line of its own in the input. In tokens it is just the next item, so the third class gets all three of its marks. - Line 10.
for i in range(n):has no number left: a range stops before its stop value,3, so the loop ends. - Line 12.
total = sum(marks)setstotalto61(it was200). - Line 13.
above = sum(1 for m in marks if m * n > total)setsaboveto1(it was0). - Line 14.
out.append(str(above))adds'1'to the end ofout. - Line 7.
for _ in range(int(next(it))):has no number left: a range stops before its stop value,3, so the loop ends. - Line 15.
print("\n".join(out))prints 3 lines, starting with2. That was the last line, so the program ends. One read and one walk over the tokens gave 2, 0 and 1, the same answers that template.py prints for the same input.
The split turned the eight lines of input into one flat list of 16 tokens, and next(it) took them in order: the 5
said how many marks followed, and 31, alone on its line, arrived like any other token.
Reading sys.stdin.buffer gives bytes, not text. Numbers do not care, because int() and float() accept
bytes, but comparing a token with a string does:
# What sys.stdin.buffer.read() returns for this input: bytes, not str
raw = b"YES 42\n-7 3.5\nRavi Kumar\n"
tokens = raw.split()
print(tokens)
print(int(tokens[1]) + int(tokens[2]), float(tokens[3])) # int() and float() accept bytes
print(tokens[0] == "YES") # bytes never equal str: False
print(tokens[0] == b"YES") # compare with a bytes literal
print(tokens[0].decode() == "YES") # or decode the token first
lines = raw.splitlines() # for a line with spaces in it
print(lines[2].decode()) Output
[b'YES', b'42', b'-7', b'3.5', b'Ravi', b'Kumar'] 35 3.5 False True True Ravi Kumar
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 bytes_tokens.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Comparing a bytes token with a str
tokens[0] == "YES" is False and raises no error, the quietest bug in the template. Compare with a bytes literal
such as b"YES", or call .decode() on the tokens that are text, as the last two comparisons above do.
A line that contains spaces, such as a name, is split into several tokens; read such inputs line by line with
splitlines() instead.
Checkpoint
Check: The read-once template and its tokens
Reading everything in one call is many times faster
How much does the way of reading matter? This program writes a file of 1,000,000 numbers, one per line, and runs four small programs on it, each in a fresh Python with the file on its standard input, exactly as a judge would. Each one adds the numbers up and reports how long its reading took:
"""Reads 1,000,000 numbers from standard input four ways, each in a fresh Python, as a judge would run it.
Each way runs three times and the fastest run counts: slower runs are mostly other programs getting in the way."""
import os
import subprocess
import sys
import tempfile
import tracemalloc
N = 1_000_000
READERS = {
"input() per line": "total = sum(int(input()) for _ in range(n))",
"sys.stdin.readline() per line": "readline = sys.stdin.readline\ntotal = sum(int(readline()) for _ in range(n))",
"sys.stdin.read().split()": "total = sum(map(int, sys.stdin.read().split()))",
"sys.stdin.buffer.read().split()": "total = sum(map(int, sys.stdin.buffer.read().split()))",
}
# The child times only its reading and adding, then prints the sum and the milliseconds.
CHILD = """import sys, time
n = {n}
start = time.perf_counter()
{body}
print(total, round((time.perf_counter() - start) * 1000))
"""
with tempfile.TemporaryDirectory() as folder:
path = os.path.join(folder, "input.txt")
with open(path, "w") as f:
f.write("\n".join(str(i * 7 % 1_000_003) for i in range(N)) + "\n")
for name, body in READERS.items():
runs = []
for _ in range(3):
with open(path, "rb") as stdin:
run = subprocess.run(
[sys.executable, "-c", CHILD.format(n=N, body=body)],
stdin=stdin, capture_output=True, text=True, check=True,
)
runs.append(tuple(map(int, run.stdout.split())))
total, ms = min(runs, key=lambda r: r[1])
print(f"{name:32} sum {total:,} in {ms:>5,} ms")
with open(path, "rb") as f:
data = f.read()
tracemalloc.start()
tokens = data.split()
print(f"memory for the {len(tokens):,} tokens of one split(): {tracemalloc.get_traced_memory()[0] / 1e6:.1f} MB") Output
input() per line sum 499,999,500,036 in 2,053 ms sys.stdin.readline() per line sum 499,999,500,036 in 135 ms sys.stdin.read().split() sum 499,999,500,036 in 86 ms sys.stdin.buffer.read().split() sum 499,999,500,036 in 76 ms memory for the 1,000,000 tokens of one split(): 47.4 MB
This output changes from run to run: it measures time, which differs from one run to the next and from one computer to another; the computer of this run is named below the output.
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 read_speed.py
All four agree on the sum; they differ in time by a factor of about twenty or more. input() is built for a person
at a keyboard: every call does work that a judge’s input does not need, from handling a possible prompt to raising
two auditing events, before it reads one line, and a million calls of that extra work cost seconds.
sys.stdin.readline() reads one line per call without that overhead, which is fine when the input really is line
by line. Reading everything with read().split() is the fastest and the simplest, and sys.stdin.buffer skips
decoding the bytes into text, which saves a little more.
The price is memory, shown on the last line: every token is a small bytes object, about 50 bytes each with its place in the list, and all of them exist at once. A few million numbers fit easily in a memory limit of 256 MB; for an input far larger than the memory limit, read it line by line instead.
Key idea
One sys.stdin.buffer.read().split() replaces a million calls of input(): the reading takes a fraction of the
time, and the cost is every token held in memory at once.
Checkpoint
Check: Reading the whole input at once
Write the output once; flush only for an interactive judge
Output has the same problem in reverse: one print() per answer is a function call per line. This program writes
200,000 lines three ways, each into a file, which is where a judge’s output goes:
"""Writes 200,000 answer lines three ways, each in a fresh Python with its output going to a file, as on a judge.
Each way runs three times and the fastest run counts."""
import os
import subprocess
import sys
import tempfile
N = 200_000
WRITERS = {
"print() per line": "for i in range(n):\n print(i)",
"print(..., flush=True) per line": "for i in range(n):\n print(i, flush=True)",
"one join, one write": 'sys.stdout.write("\\n".join(map(str, range(n))) + "\\n")',
}
# The child writes its answers to standard output and its time, in milliseconds, to standard error.
CHILD = """import sys, time
n = {n}
start = time.perf_counter()
{body}
sys.stdout.flush()
print(round((time.perf_counter() - start) * 1000), file=sys.stderr)
"""
with tempfile.TemporaryDirectory() as folder:
path = os.path.join(folder, "output.txt")
for name, body in WRITERS.items():
times = []
for _ in range(3):
with open(path, "w") as out:
run = subprocess.run(
[sys.executable, "-c", CHILD.format(n=N, body=body)],
stdout=out, stderr=subprocess.PIPE, text=True, check=True,
)
times.append(int(run.stderr))
print(f"{name:32} {os.path.getsize(path):,} bytes in {min(times):>5,} ms") Output
print() per line 1,288,890 bytes in 59 ms print(..., flush=True) per line 1,288,890 bytes in 1,181 ms one join, one write 1,288,890 bytes in 19 ms
This output changes from run to run: it measures time, which differs from one run to the next and from one computer to another; the computer of this run is named below the output.
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 write_speed.py
All three files are the same size. Joining the lines and writing once is the fastest. Plain print() per line is
slower but still reasonable, because when standard output is a file or a pipe, Python collects it in an
output buffer: Memory where written text waits before it is sent on: a line at a time when standard output is a terminal, in larger blocks for a file or a pipe. print(..., flush=True) sends it at once. More in “Input and output: print(), input() and stdin” and writes it in large blocks. flush=True gives up that saving:
every line becomes its own write to the operating system, and the same output takes many times as long. Step through
what the buffer does with each line, first in an ordinary problem and then in an interactive one:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- Lines collect. When standard output is a file or a pipe, as on a judge, print() puts each line into the output buffer instead of handing it to the operating system at once.
- A few large writes. The buffer goes out in large blocks, when it fills and when the program ends. That is why plain print() stayed fast in the measurement above.
- An interactive problem. In an interactive problem the judge answers each query before the next one. The program prints its first guess, "? 50", and calls sys.stdin.readline() to read the reply.
- Both sides wait. The guess is still in the buffer, so the judge has not seen it and waits for a line. The program waits for the reply. Neither moves until the time limit ends the run.
- flush=True sends it. print("? 50", flush=True) empties the buffer straight away, so the line reaches the judge the moment it is printed.
- The reply comes back. The judge reads 50 and answers on the program's standard input. The program reads the reply and prints its next guess, flushed again, one line per query.
Note
interactive problem: A coding-round problem in which the judge reads each line the program prints and replies before the next one. The program must flush its output after every query, or both sides wait until the time limit ends the run. are the exception. There the judge reads each line
you print and answers it before you continue, so a line left in the buffer leaves both programs waiting for each
other until the time limit. In an interactive problem, use print(..., flush=True) (or sys.stdout.flush()) after
every query; everywhere else, never flush line by line.
Checkpoint
Check: Writing once, flushing for interactive judges
Recursion stops near 1,000 frames; a list of your own does not
Every unfinished call keeps a frame: The record of one function call that has not finished: its local variables and the place to carry on when the call it is waiting for returns. More in “Recursion, the call stack and its limits”, and CPython stops at a recursion limit: The most frames CPython allows on its call stack at once, 1,000 by default, counting the frames below your first call. Going past it raises RecursionError. More in “Recursion, the call stack and its limits” of 1,000 frames by default. That is plenty for most recursion, but not for a depth-first search over a graph shaped like a long path, because the search goes one frame deeper for every node:
import sys
N = 100_000
# A path 0 - 1 - 2 - ... - 99,999: the deepest graph a depth-first search can meet
graph = [[] for _ in range(N)]
for v in range(N - 1):
graph[v].append(v + 1)
graph[v + 1].append(v)
def dfs_recursive(v, seen):
seen.add(v)
for w in graph[v]:
if w not in seen:
dfs_recursive(w, seen)
def dfs_iterative(start):
seen = {start}
stack = [start] # the nodes still to explore, in a list of our own
while stack:
v = stack.pop()
for w in graph[v]:
if w not in seen:
seen.add(w)
stack.append(w)
return seen
print("recursion limit:", sys.getrecursionlimit())
seen = set()
try:
dfs_recursive(0, seen)
print(f"recursive DFS visited {len(seen):,} nodes")
except RecursionError as error:
print(f"recursive DFS stopped with {type(error).__name__} after {len(seen):,} nodes")
print(f"iterative DFS visited {len(dfs_iterative(0)):,} nodes") Output
recursion limit: 1000 recursive DFS stopped with RecursionError after 999 nodes iterative DFS visited 100,000 nodes
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 deep_dfs.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Version note
In the browser, the code that starts your program keeps a few frames of its own beneath it, so Run stops the recursive search a few nodes before 999. Those frames and the search’s still add up to the limit of 1,000.
In your browser, Pyodide 314.0.7 (CPython 3.14.2) prints:
recursion limit: 1000 recursive DFS stopped with RecursionError after 994 nodes iterative DFS visited 100,000 nodes
Step through what the recursive search does to the call stack, and what the iterative search keeps instead:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- The module's frame. Before any search starts, the module's own code is running in a frame. It is frame 1 of the 1,000 that sys.getrecursionlimit() reported.
- One frame per node. dfs_recursive(0) visits node 0 and calls itself on its neighbour 1, which calls itself on 2, and so on. None of them can return before the end of the path, so every node keeps a frame.
- The last frame allowed. dfs_recursive(998) runs in frame 1,000, the last one the limit allows. By now the search has visited nodes 0 to 998, which is 999 nodes.
- One call too many. Calling dfs_recursive(999) would need frame 1,001, so Python raises RecursionError instead. The program prints that it stopped after 999 nodes of 100,000.
- A list instead. dfs_iterative() keeps the nodes still to explore in a list of its own and takes the last one each time. A list lives in ordinary memory, so it has no limit like the call stack's.
- One node at a time. On this path the list never holds more than one node. The loop takes 0 and puts 1 on the list, then takes 1 and puts 2 on it, all inside the one call of dfs_iterative().
- Every node visited. The loop ends when the list is empty, with all 100,000 nodes in seen, and the program prints 100,000. The depth of the graph never mattered.
The iterative version keeps the nodes still to explore in a list of its own, an explicit stack: A list that a loop uses to keep the work still to do, in place of recursive calls: append() pushes an item and pop() takes the last one. Only memory limits its size. More in “Recursion, the call stack and its limits”, and takes the last one each time, as the call stack does, but without its limit. A list holds millions of items, so the depth no longer matters. The order in which neighbours are visited can differ from the recursive version, which rarely matters for a search; when it does, push the neighbours in reverse order and mark a node as seen when it is taken from the list, not when it is put on it.
Raising the limit is the other common answer, and it works only for some recursion. Since Python 3.11, most calls
from Python code to a Python function use no C stack at all, so plain recursion can go much deeper once the limit is
raised. A call that passes through C code, such as a function wrapped by functools.cache, still uses the C stack,
which has a fixed size:
import functools
import sys
import threading
sys.setrecursionlimit(1_000_000)
def depth(n):
return 0 if n == 0 else 1 + depth(n - 1)
@functools.cache
def cached_depth(n):
return 0 if n == 0 else 1 + cached_depth(n - 1)
def attempt(f, n):
try:
return f"{f(n):,} levels"
except RecursionError as error:
return "RecursionError: " + str(error).split(" (")[0]
print("plain function, 100,000 deep: ", attempt(depth, 100_000))
print("cached function, 10,000 deep: ", attempt(cached_depth, 10_000))
# The workaround some judge solutions use: a thread with a much larger stack
threading.stack_size(512 * 1024 * 1024)
cached_depth.cache_clear()
result = []
worker = threading.Thread(target=lambda: result.append(attempt(cached_depth, 100_000)))
worker.start()
worker.join()
print("cached function, 100,000 deep, 512 MiB thread stack:", result[0]) Output
plain function, 100,000 deep: 100,000 levels cached function, 10,000 deep: RecursionError: Stack overflow cached function, 100,000 deep, 512 MiB thread stack: 100,000 levels
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 deep_limit.py
With the limit at a million, the plain function went 100,000 levels deep, but the cached one failed on a recursion
10,000 levels deep: CPython 3.14 protects its C stack and raised RecursionError with the message “Stack overflow”
instead of crashing. A thread created after threading.stack_size() gets a larger stack, and with 512 MiB the
cached function reached 100,000 levels. That workaround depends on the platform and on how much memory the judge
allows, so treat it as a last resort; the documentation of sys.setrecursionlimit() adds that a very high limit can
make Python crash. The explicit stack works everywhere.
Key idea
When the depth of a recursion grows with the input, rewrite it as a loop over a list of your own: neither the 1,000-frame limit nor the C stack applies to a list.
Checkpoint
Check: Deep recursion and the explicit stack
Python ints never overflow, but their decimal text stops at 4,300 digits
Python’s integers grow as large as memory allows, so sums and products never overflow as they can in C++ or Java. Two limits remain:
import math
import sys
E = 20_000 # the exponent: try 14_284, then 14_285
x = 2 ** E # Python ints never overflow
print("bits:", x.bit_length())
print("decimal digits:", math.floor(math.log10(x)) + 1)
try:
print(str(x))
except ValueError as error:
print("ValueError:", error)
print("hex digits:", len(hex(x)) - 2) # power-of-two bases have no limit
sys.set_int_max_str_digits(0) # 0 removes the limit, for this program only
print("first digits:", str(x)[:12])
MOD = 10**9 + 7 # judges often ask for the answer modulo a prime like this
print(f"2 ** {E} % MOD =", x % MOD, "=", pow(2, E, MOD)) Output
bits: 20001 decimal digits: 6021 ValueError: Exceeds the limit (4300 digits) for integer string conversion; use sys.set_int_max_str_digits() to increase the limit hex digits: 5001 first digits: 398027684033 2 ** 20000 % MOD = 614428880 = 614428880
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 big_ints.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
- Text conversion. Turning an
intinto decimal text, or decimal text into anint, is not a linear-time job for huge numbers, so CPython refuses numbers of more than 4,300 decimal digits by default, a protection against inputs that would keep a server busy.hex(),bin()andoct()have no limit, andsys.set_int_max_str_digits(0)removes it for one program when a problem really asks you to print a huge number. - Speed. Arithmetic on a number with thousands of digits takes far longer than on a small one. When a problem
asks for the answer “modulo 10⁹ + 7”, it wants you to reduce after every step so that the numbers stay small:
(a * b) % MODin a loop, orpow(a, b, MOD), which computes a modular power without ever building the huge number. The example checks that both ways give the same remainder.
Find the edge of the limit
Press Edit, change E = 20_000 to E = 14_284 and run: that power of 2 has exactly 4,300 digits, and str() prints
it in full. Change it to 14_285, one digit more, and the ValueError is back.
Checkpoint
Check: Big ints, the digit limit and modular answers
Check which Python the judge runs before you use a new feature
Judges list the language versions they offer, and code that uses a feature newer than the judge’s Python fails there with a runtime error, even though it works at home. The first submission to a new judge can print the version to standard error, and features can be checked at run time:
import heapq
import itertools
import math
import sys
print("implementation:", sys.implementation.name)
print("version:", "%d.%d" % sys.version_info[:2])
print("math.lcm, Python 3.9+:", hasattr(math, "lcm"))
print("itertools.batched, Python 3.12+:", hasattr(itertools, "batched"))
print("heapq.heappush_max, Python 3.14+:", hasattr(heapq, "heappush_max")) Output
implementation: cpython version: 3.14 math.lcm, Python 3.9+: True itertools.batched, Python 3.12+: True heapq.heappush_max, Python 3.14+: True
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 version_check.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
math.lcm() needs Python 3.9, itertools.batched() 3.12 and heapq.heappush_max() 3.14. Walk through the versions
that added the features solutions use most:
- 3.8
math.isqrt(), math.comb() and modular inverses new functions
math.isqrt() gives exact integer square roots without floats, math.comb() and math.perm() count choices, and pow(a, -1, m) computes a modular inverse.
- 3.9
math.lcm(), and gcd() of many numbers new functions
math.lcm() is added, and math.gcd() accepts any number of arguments instead of exactly two.
- 3.10
bisect with key, zip() with strict new parameters
The bisect functions accept a key function, zip(..., strict=True) insists on inputs of equal length, and int.bit_count() counts the ones in a number's binary form.
- 3.11
Faster code, and calls without C stack faster
CPython 3.11 is about 25 % faster than 3.10 on its benchmark suite, and most calls between Python functions use no C stack, so plain recursion can go deeper once the limit is raised.
- 3.12
itertools.batched() new function
itertools.batched() cuts an iterable into tuples of n items, the last one possibly shorter. 3.12 is also the newest language version that PyPy supports.
- 3.14
Max-heaps in heapq new functions
heapq gains heapify_max(), heappush_max(), heappop_max(), heapreplace_max() and heappushpop_max(), functions for heaps that keep the largest item first.
The timeline has 6 events, from 3.8 to 3.14.
Some judges also offer PyPy: A separate implementation of the Python language with a just-in-time compiler, which turns code that runs often into machine code while the program runs. It implements an older version of the language than the newest CPython., a separate implementation of Python with a just-in-time compiler. Its project reports that, on average, PyPy 3.12 runs about four times as fast as CPython 3.11, and it supports the language of Python 3.12 and 3.11, so features added in 3.13 or 3.14 are missing there. Submit to PyPy when a correct solution is close to the time limit and uses nothing newer than its version.
Checkpoint
Check: The judge's Python version
Practise: three judge-style problems at full size
Practise with problems that are original or openly licensed, such as the ones in this track and the Data Structures & Algorithms (DSA) and coding-patterns tracks of MySmartCoPilot. The questions of companies’ online assessments belong to those companies, and MySmartCoPilot never republishes them; a question that “was asked” somewhere teaches nothing that an original problem of the same pattern does not. Before you submit any solution, run six checks:
- Run the largest input. Generate one at the limits of the statement with a few lines of Python, and time the whole program on it, reading and writing included.
- Clean standard output. No prompts, no debugging lines: only the answers, in the order asked.
- Match the format exactly. One answer per line unless told otherwise, the exact spelling of words such as
YES, and the number of decimal places the statement asks for. - Remove deep recursion. Any recursion whose depth grows with the input becomes a loop with a stack.
- Reduce as you go. When the answer is asked modulo a prime, take the remainder after every step.
- Check the version. Use only what the judge’s Python, or PyPy, provides.
The exercise has three problems at the sizes a judge would use. Each function receives the whole input as bytes and returns the whole output, and the sample tests include inputs at the limits:
Exercise · Medium · Python
Three judge-style problems at full input size
Write three functions in judge.py. Each one receives the whole input of a test as bytes, exactly what sys.stdin.buffer.read() returns on a judge, and returns the whole output as one string, with one answer per line. A main() that reads standard input and writes your result would be the only thing left to add before submitting. Spaces at the ends of lines and a final line break do not matter.
range_sums(data). The first line holdsnandq, the second linenwhole numbersa1 … an, and each of the nextqlines a pairl rwith 1 ≤ l ≤ r ≤ n. Print the suma_l + … + a_rfor each pair. Up to 200,000 numbers and 200,000 pairs, each number between -10⁹ and 10⁹.frequency_of_frequencies(data). The first token isn, followed bynwords of small letters, separated by spaces or line breaks. For every countcthat some word has, in increasing order ofc, printc k:kwords occur exactlyctimes. Up to 200,000 words.grid_steps(data). The first line holdsrandc, thenrlines ofccharacters:.is open,#is a wall,Sis the start andEthe exit, each exactly once. Print the least number of moves fromStoE, moving up, down, left or right through cells that are not walls, or-1if the exit cannot be reached. Up to 300 rows and 300 columns.
The sample tests include inputs at those limits and must finish within the time limit of the Run button, so a solution that is slower than about n log n for any of the three will not pass.
Starter code · judge.py
from collections import Counter, deque
def range_sums(data):
"""The sum of a[l..r] for every pair l r, one per line."""
return ""
def frequency_of_frequencies(data):
"""Lines "c k": k distinct words occur exactly c times, by increasing c."""
return ""
def grid_steps(data):
"""The least number of moves from S to E, or -1."""
return "" The sample tests · test_judge.py
from judge import frequency_of_frequencies, grid_steps, range_sums
def lines(text):
"""The output's lines, without spaces at their ends or blank lines at the end."""
return [line.rstrip() for line in str(text).strip().splitlines()]
def letters(i):
"""A different word of small letters for every number i."""
word = ""
while True:
i, r = divmod(i, 26)
word += "abcdefghijklmnopqrstuvwxyz"[r]
if i == 0:
return word
def test_range_sums_sample():
"""answers each pair with the sum of its range"""
data = b"5 4\n3 -1 4 1 5\n1 5\n2 3\n4 4\n1 1\n"
assert lines(range_sums(data)) == ["12", "3", "1", "3"]
def test_range_sums_at_the_limits():
"""answers 200,000 pairs over 200,000 numbers in time"""
n = 200_000
numbers = " ".join(str(i if i % 2 else -i) for i in range(1, n + 1)) # 1 -2 3 -4 ...
pairs = [(i, n - i + 1) for i in range(1, n // 2 + 1)] + [(i, i) for i in range(1, n // 2 + 1)]
def first(k): # 1 - 2 + 3 - 4 ... up to the k-th number
return (k + 1) // 2 if k % 2 else -(k // 2)
text = f"{n} {len(pairs)}\n{numbers}\n" + "\n".join(f"{l} {r}" for l, r in pairs) + "\n"
out = lines(range_sums(text.encode()))
assert len(out) == len(pairs)
assert out[:3] == ["-100000", "99999", "-99998"]
assert out == [str(first(r) - first(l - 1)) for l, r in pairs]
def test_frequency_sample():
"""counts how many words share each count"""
data = b"9\ntea coffee tea\nmilk tea water\ncoffee milk juice\n"
assert lines(frequency_of_frequencies(data)) == ["1 2", "2 2", "3 1"]
def test_frequency_at_the_limits():
"""handles 200,000 words in time"""
words = []
for i in range(1, 501): # one word that occurs once, one twice ... one 500 times
words += ["w" + letters(i)] * i
words += ["z" + letters(i) for i in range(74_750)] # and 74,750 words that occur once
assert len(words) == 200_000
out = lines(frequency_of_frequencies(f"{len(words)}\n{' '.join(words)}\n".encode()))
assert out == ["1 74751"] + [f"{c} 1" for c in range(2, 501)]
def test_grid_sample():
"""finds the shortest way round the walls"""
data = b"4 5\nS..#.\n.#.#.\n.#...\n...#E\n"
assert lines(grid_steps(data)) == ["7"]
def test_grid_all_directions():
"""moves up and left as well as down and right"""
assert lines(grid_steps(b"3 5\nS#.#E\n.#.#.\n.....\n")) == ["8"]
assert lines(grid_steps(b"3 5\nE#.#S\n.#.#.\n.....\n")) == ["8"]
def test_grid_unreachable():
"""prints -1 when the exit is walled off"""
data = b"3 3\nS#.\n##.\n..E\n"
assert lines(grid_steps(data)) == ["-1"]
def test_grid_at_the_limits():
"""searches a 300 x 300 grid in time"""
rows = ["." * 300 for _ in range(300)]
rows[0] = "S" + rows[0][1:]
rows[-1] = rows[-1][:-1] + "E"
assert lines(grid_steps(("300 300\n" + "\n".join(rows) + "\n").encode())) == ["598"] A hint
Split the bytes once with data.split() and walk through the tokens with an iterator, converting with int() only the tokens that are numbers. For range_sums, build the prefix sums first, with prefix[i] the sum of the first i numbers, so that every pair is answered by one subtraction, prefix[r] - prefix[l - 1]. For frequency_of_frequencies, count twice: Counter of the words, then Counter of those counts. For grid_steps, the grid lines are tokens too (they contain no spaces); search breadth first from S with a collections.deque, so that the first time you reach a cell is along a shortest path.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Last, test the whole lesson on yourself. The practice below opens with whichever idea you have answered least well, and every answer is explained straight away:
Practice that adapts to you
Practise this lesson
15 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Fresher · Why do coding-round solutions read sys.stdin.buffer instead of calling input() in a loop?
Because the input of a judge’s test arrives all at once, and reading it in one call is both faster and simpler.
input() is designed for a person typing: every call can write a prompt, raises auditing events and returns one
line as text, so a million calls add up to seconds, while one sys.stdin.buffer.read().split() reads the same
million numbers in about a tenth of a second. Splitting on any whitespace also stops caring how the numbers are
spread over lines. buffer skips decoding bytes to text; the tokens are bytes, which int() accepts, but compare
them with bytes literals or decode them first. The cost is memory, since every token is held at once, which is fine
for typical limits. For line-structured input, sys.stdin.readline is a good middle way.
Key takeaways
- A judge feeds each test on standard input and compares everything you print, so prompts and debugging lines stay off standard output, and one failing test fails the whole submission.
- Read the whole input with
sys.stdin.buffer.read().split()and walk the tokens withnext(it): the line breaks stop mattering, the reading is many times faster, and the tokens are bytes. - Collect the answers and write them with one
sys.stdout.write("\n".join(lines)); flush line by line only in interactive problems, where the judge waits for each query. - Recursion stops near 1,000 frames by default, and raising the limit does not help recursion through C code; a loop with a list of your own works on any judge.
intnever overflows, but decimal text conversion stops at 4,300 digits by default, and modular answers want% MODafter every step orpow(a, b, MOD).- A feature added in a recent Python fails on a judge with an older one, so check the judge’s version, or PyPy’s, before you rely on it.
Terms in this lesson
- interactive problem
- A coding-round problem in which the judge reads each line the program prints and replies before the next one. The program must flush its output after every query, or both sides wait until the time limit ends the run.
- online judge
- A service that runs a submitted program on hidden tests: it gives each test's input on standard input, compares everything the program prints with the expected answer, and gives a verdict for every test, under a time limit and a memory limit.
- PyPy
- A separate implementation of the Python language with a just-in-time compiler, which turns code that runs often into machine code while the program runs. It implements an older version of the language than the newest CPython.
References
- sys.stdin, sys.stdout and sys.stderr (Python Software Foundation)
- Built-in functions: input() (Python Software Foundation)
- Built-in functions: print() (Python Software Foundation)
- sys.setrecursionlimit() (Python Software Foundation)
- What's New in Python 3.11: inlined Python function calls (Python Software Foundation)
- threading.stack_size() (Python Software Foundation)
- Integer string conversion length limitation (Python Software Foundation)
- collections.deque (Python Software Foundation)
- PyPy: a fast, compliant alternative implementation of Python (The PyPy Team)
- What's New in Python 3.8: math and pow() (Python Software Foundation)
- What's New in Python 3.9: math (Python Software Foundation)
- What's New in Python 3.10: bisect, zip() and int.bit_count() (Python Software Foundation)
- What's New in Python 3.11: Faster CPython (Python Software Foundation)
- What's New in Python 3.12: itertools (Python Software Foundation)
- What's New in Python 3.14: heapq (Python Software Foundation)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress