Skip to main content

OnlineCompiler.io Python client

Run code in 46 languages from Python, or judge it against test cases, through the OnlineCompiler.io code execution API. Every run gets a fresh gVisor sandbox with no network, 512 MB of memory and a 30-second limit. Free up to 100,000 runs a month, commercial use included.

No dependencies beyond the standard library. Python 3.10 or newer. Verifying signed receipts needs one optional extra, pip install "onlinecompiler[receipts]" (it adds cryptography).

pip install onlinecompiler

Get a free API key at https://api.onlinecompiler.io/compilers/api-keys/ and set it as ONLINECOMPILER_API_KEY, or pass it to the client.

Run a program

from onlinecompiler import OnlineCompiler

client = OnlineCompiler()  # reads ONLINECOMPILER_API_KEY

result = client.run("python-3.14", "name = input()\nprint(f'Hello, {name}!')", input="Ada")
print(result["output"])                          # Hello, Ada!
print(result["exit_code"], result["time"], result["memory"])

The result has output (stdout), error (stderr or compiler errors), exit_code, signal, time and total (seconds), memory (KB), truncated, time_limit and files. time, total and memory are decimal strings, as the API sends them. Compiler ids such as gcc-15, openjdk-25 and nodejs-24 come from client.compilers(). Pass time_limit (seconds, up to 30) to stop a run sooner.

Pass args (up to 32 strings) to give the program command-line arguments, and memory_limit (whole MB, 16 to 512) to end a run whose peak memory is over it with exit code 137. Both also work on submit(); in run_tests() args goes on each test and memory_limit applies to all of them. sqlite-3, duckdb-1.5 and erlang-29 take no arguments.

Judge a solution against test cases

result = client.run_tests(
    "python-3.14",
    "a, b = map(int, input().split())\nprint(a + b)",
    tests=[
        {"input": "1 2", "expected_output": "3"},
        {"input": "10 20", "expected_output": "30", "hidden": True},
    ],
)
print(result["verdict"], result["score"], f"{result['passed']}/{result['total']}")
for case in result["tests"]:
    print(case["verdict"])

Each test counts as one run. Hidden tests return only their verdict. checker picks how output is compared (trim by default, exact, tokens, float or rows), scoring is all_or_nothing (default), percent or weighted (with a weight per test), and time_limit is seconds per test (default 2). See the run-tests docs.

Every run-tests result also has summary (the result in plain words), submission (its id) and a signed receipt.

Judge in the background

queued = client.run_tests(
    "python-3.14",
    "a, b = map(int, input().split())\nprint(a + b)",
    tests=[{"input": "1 2", "expected_output": "3"}, {"input": "10 20", "expected_output": "30", "hidden": True}],
    async_=True,
    external_ref="attempt-4821",
    idempotency_key="attempt-4821",
)
done = client.wait_for_submission(queued["submission"])  # looks it up every 2 seconds
result = done["result"]  # None when it failed, which means it was not judged
if result is not None:
    print(result["verdict"], result["score"])
else:
    print("Not judged:", done["error"])  # send it again
client.delete_submission(done["submission"])  # optional: drop the stored code and output

async_=True (async is a Python keyword) answers at once with the submission id, and the submission waits in your account's queue instead of getting 429 in a burst. Add callback_url to have the result POSTed to your server as a signed webhook (see below); it needs async_=True, and the client refuses it without. external_ref is your own id, signed into the receipt; metadata is any JSON object, returned in lookups and webhooks. idempotency_key (1 to 100 printable ASCII characters, checked before sending) is sent as the Idempotency-Key header, so a retry after a timeout answers with the original result instead of running again. client.submission(id) looks a submission up once.

List submissions

import datetime

for entry in client.iter_submissions(external_ref="candidate-8841"):
    print(entry["submission"], entry["status"], entry["verdict"], entry["score"])

page = client.submissions(problem="sum-two", verdict="accepted", since=datetime.date(2026, 11, 1), limit=50)
more = client.submissions(problem="sum-two", verdict="accepted", since=datetime.date(2026, 11, 1), limit=50,
                          cursor=page["next_cursor"])  # when next_cursor is not None

client.submissions() returns one page of your submissions, newest first, filtered by ids, external_ref, problem (id or slug), status, verdict, since (included) and until (not included). since and until take a datetime (one without a zone is UTC), a date or ISO 8601 text. Each entry has the verdict, score and tests passed, but not the tests or receipt: client.submission(id) has those. client.iter_submissions() fetches page after page for you. With ids alone, every id comes back on one page. Draft tests, reference checks and Submits under a test session made in the dashboard are not listed.

Judge against a stored problem

A stored problem keeps its tests, checker and limits on the server, so you send only the candidate's code and the hidden tests never leave it.

client.create_problem({
    "title": "Sum of two numbers",
    "slug": "sum",
    "languages": ["python-3.14"],
    "tests": [
        {"name": "sample", "input": "1 2\n", "expected_output": "3\n", "hidden": False},
        {"input": "5 7\n", "expected_output": "12\n"},  # tests are hidden unless you say otherwise
    ],
})
client.publish_problem("sum")  # no reference solution, so it is published at once

result = client.judge("sum", "python-3.14", "a, b = map(int, input().split())\nprint(a + b)")
print(result["verdict"], result["score"], result["problem"], result["version"])

Pass tests=[1, 3] (1-based positions) to judge only those tests; the submission's mode becomes "tests". Stored-problem lookups and lists show mode and rejudge_of.

judge takes the problem's id (prob_...) or your slug for it. mode="samples" judges only the visible tests, and async_, callback_url, external_ref, metadata and idempotency_key work as above. Problems are plain dicts, as the stored problems docs describe them: problems(), problem(), create_problem(), update_problem(), replace_draft(), publish_problem(), archive_problem(), restore_problem() and public_problem().

A problem's data files (a SQLite database, a CSV, any bytes the tests read) go in the draft's files ({"name", "content", "encoding"}) through create_problem() or replace_draft(), or one at a time as raw bytes, which suits large or binary files. Owner bodies list them as files: [{name, bytes, sha256}].

client.put_draft_file("sum", "orders.db", open("orders.db", "rb").read())  # str is sent as UTF-8
client.delete_draft_file("sum", "orders.db")  # NotFoundError when absent

Re-judge earlier submissions

After publishing a new version, run earlier submissions against it. Each is queued as a new async submission with rejudge_of set to the original; at most 100 a call.

result = client.rejudge(
    "sum",
    version=2,  # default: the current version
    submissions=["sub_..."],  # default: every earlier submission that qualifies
    notify=True,  # send the original's callback_url a webhook; default False
)
print(len(result["queued"]), result["remaining"])

Call again while remaining is above 0. Every argument is optional.

Sessions for the widget

A session lets one candidate's embedded editor Submit to a stored problem without your API key ever reaching the browser. Create it on your server and put only its token in the page:

problem_id = client.problem("sum")["problem"]  # the widget takes the id, never the slug
session = client.create_session(problem_id, "attempt-4821", max_submits=2, expires_in=3600)

html = (f'<div class="runcode" data-key="YOUR_WIDGET_KEY" data-problem="{problem_id}" '
        f'data-session="{session["token"]}"></div>\n'
        '<script src="https://onlinecompiler.io/widget.js"></script>')

The token is shown only this once and the client never logs it or puts it in an error, so keep it out of your own logs too. external_ref should be an opaque id: the candidate's page sees it. Record the score from the webhook or client.submission(id), never from the browser.

Each Submit's signed receipt is shared, so the widget links to its public page; the page shows external_ref and your account reference, never the code, tests or output. Pass share_receipts=False to keep a session's receipts private. client.session(problem_id, session["session"]) shows the Submits used and the submission ids, and client.revoke_session(problem_id, session["session"]) ends it at once.

Verify webhooks

An async submission with a callback_url (or a callback URL on the API key) gets one signed submission.finished event. Check it before you trust it.

Verify the raw request body exactly as received: a body that was parsed and serialized again never matches. In Flask that is request.get_data(), in Django request.body, in FastAPI await request.body().

import os

from flask import Flask, abort, request
from onlinecompiler import verify_webhook

app = Flask(__name__)
SECRET = os.environ["ONLINECOMPILER_WEBHOOK_SECRET"]  # whsec_..., shown with the API key in your dashboard

@app.post("/hooks/oc")
def webhook():
    if not verify_webhook(SECRET, request.get_data(), request.headers):
        abort(400)
    event = request.get_json()
    print(event["submission"], event["status"], event["result"] and event["result"]["verdict"])
    return "", 204  # answer quickly; deduplicate on event["delivery"]

verify_webhook(secret, raw_body, headers, tolerance=300) needs no client and no network. It takes any mapping of headers (Django's, Flask's, FastAPI's or a dict), compares in constant time, accepts either signature while a secret is being rotated, takes a list of secrets while you rotate yours, and rejects a timestamp more than tolerance seconds (default 300) from now. Anything missing or malformed returns False. See the webhooks docs.

Verify receipts

Every judged submission comes with a receipt signed with Ed25519: proof of what ran, against which tests, and what came out. Anyone can check it with the published keys.

pip install "onlinecompiler[receipts]"
from onlinecompiler import verify_receipt

receipt = client.submission("sub_8f2Kq7LmT3xR9vWb1cYd4hNe")["receipt"]
print(client.verify_receipt(receipt))  # fetches the published keys once, no API key needed

keys = client.receipt_keys()           # or save them and verify offline later
print(verify_receipt(receipt, keys))   # False once any field has been changed

Both return False for None, which a submission that was not judged has as its receipt. Without the extra, verifying raises an error that says what to install. Store and deduplicate receipts by their submission id (or the whole receipt), not by the signature string. canonical_json(value) returns the canonical bytes receipts are signed over, for reproducing their hashes as the receipts docs describe.

Files in and out

import base64

code = """
import pandas as pd
import matplotlib.pyplot as plt
df = pd.read_csv("sales.csv")
df.plot(x="month", y="units")
plt.show()   # saved as figure-1.png in $OUTPUT_DIR
"""
result = client.run("python-3.14", code, files=[{"name": "sales.csv", "content": open("sales.csv").read()}])
for f in result["files"]:
    with open(f["name"], "wb") as out:
        out.write(base64.b64decode(f["content_base64"]))

Input files sit in the program's working directory; send binary ones with "encoding": "base64". Files the program writes to $OUTPUT_DIR come back in files.

Queue runs and get results by callback

queued = client.submit("g++-15", source, input="3\n", extra_params={"submission_id": 981})
print(queued["id"])

submit returns at once. When the run finishes, its result is POSTed to the callback URL set on your API key in the dashboard, with your extra_params echoed back.

Other calls

  • client.compilers(): every compiler id and language name (no key needed).
  • client.usage(): this month's runs and what's left, for the account and for the key.
  • client.limits(): per-run limits and default account limits (no key needed).

Errors

from onlinecompiler import OnlineCompilerError, RateLimitError

try:
    client.run("python-3.14", "print(1)")
except RateLimitError as e:          # HTTP 429
    print(e.message)                 # which limit was hit, and how to raise it
except OnlineCompilerError as e:     # anything else, including AuthenticationError
    print(e.status, e.message)       # status is 0 when the API could not be reached

AuthenticationError covers a missing, unknown or unauthorized key; NotFoundError a submission, problem or session that does not exist or is not yours. A conflict, such as an archived problem or an Idempotency-Key reused with another body, is an OnlineCompilerError with status 409 and the API's message.

The client never retries a request by itself, because every run the API accepts counts toward your allowance. OnlineCompiler(api_key=None, *, base_url="https://api.onlinecompiler.io", timeout=90.0) takes the key, the API address and a timeout in seconds.

Docs · Languages · Limits · Pricing · Security · Changelog

MIT licensed. OnlineCompiler.io is a product of RunCode Inc.

Metadata

Release files for onlinecompiler 0.6.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for onlinecompiler 0.6.0
File Size Uploaded Provenance Actions
onlinecompiler-0.6.0.tar.gz 49.0 kB Without provenance Details for onlinecompiler-0.6.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for onlinecompiler 0.6.0
File Interpreter ABI Platform Actions
onlinecompiler-0.6.0-py3-none-any.whl Python 3 none any Details for onlinecompiler-0.6.0-py3-none-any.whl

Total release size: 71.4 kB

Release files / onlinecompiler-0.6.0.tar.gz

Download URL onlinecompiler-0.6.0.tar.gz
Size 49.0 kB
Tags Source
SHA-256 checksum
How to use checksums
c85a5617abec99b789fbd43b6165c445c82d0151652ab9fb5305ddfb33b5b9e8
BLAKE2b-256 checksum
How to use checksums
7b0459f77d268c0bdb27c5119e29f59ce130750a7e088be3a57e19d9c31ba4c0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.12.12

Release files / onlinecompiler-0.6.0-py3-none-any.whl

Download URL onlinecompiler-0.6.0-py3-none-any.whl
Size 22.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
65d53a5b73c8a179c6dbd86bba27a62c95d54e78b40a3189f1037a2f2a15a92f
BLAKE2b-256 checksum
How to use checksums
f0f175103313efacfec71f9b05abe38e3acc78b220b2204fc53a30ea3e6ec1b1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.12.12

Security for onlinecompiler 0.6.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page