Production-ready, async-first captcha library for Python 3.10+. Zero runtime deps beyond Pillow. Drop-in adapters for FastAPI, aiogram, Discord and Django — plus Redis / Postgres storage, rate limiting and Prometheus metrics.
Four built-in themes, plus SVG and audio renderers — all produced by the same CaptchaManager API.
![]() Theme.CLASSIC |
![]() Theme.DARK |
![]() Theme.PASTEL |
![]() Theme.HIGH_CONTRAST · WCAG AA |
![]() MathChallengeFactory → ImageRenderer |
WordChallengeFactory → SVGRenderer (~2 KB) |
||
Accessibility: accessibility.wav — an AudioRenderer sample (~110 KB WAV) that dictates the same solution as a visual challenge, so screen-reader users get an a11y-friendly alternative.
All assets above were rendered by
scripts/render_showcase.pyusing the public API — run it yourself to regenerate them.
| Feature | lepture/captcha |
claptcha |
multicolorcaptcha |
captchakit |
|---|---|---|---|---|
| Async API | ❌ | ❌ | ❌ | ✅ |
py.typed + mypy strict |
❌ | ❌ | ✅ | |
| TTL & attempt tracking | ❌ | ❌ | ❌ | ✅ built-in |
| Pluggable storage | ❌ | ❌ | ❌ | ✅ Protocol — Memory / Redis / Postgres |
| Pluggable rate limiter | ❌ | ❌ | ❌ | ✅ Protocol — in-memory & Redis token-bucket |
| Prometheus metrics | ❌ | ❌ | ❌ | ✅ opt-in |
| Framework adapters | ❌ | ❌ | ❌ | ✅ FastAPI · aiogram · Discord · Django |
| Audio challenge (a11y) | ❌ | ❌ | ❌ | ✅ AudioRenderer |
| i18n prompt hooks | ❌ | ❌ | ❌ | ✅ en / tr / de / es + custom catalog |
| Core runtime deps | +Pillow | +Pillow | +Pillow | +Pillow |
pip install captchakit # core
# adapters
pip install "captchakit[fastapi]"
pip install "captchakit[aiogram]"
pip install "captchakit[discord]"
pip install "captchakit[django]"
# storage
pip install "captchakit[redis]" # + rate-limit token bucket
pip install "captchakit[postgres]"
# observability
pip install "captchakit[metrics]" # Prometheus adapterimport asyncio
from captchakit import (
CaptchaManager, ImageRenderer, MemoryStorage, TextChallengeFactory,
)
async def main() -> None:
manager = CaptchaManager(
factory=TextChallengeFactory(length=5),
renderer=ImageRenderer(),
storage=MemoryStorage(),
ttl=120.0,
max_attempts=3,
)
challenge_id, png_bytes = await manager.issue()
# ... show png_bytes to the user, receive their answer ...
ok = await manager.verify(challenge_id, user_input="ABCDE")
print("verified" if ok else "wrong answer, more attempts remain")
asyncio.run(main())from fastapi import Depends, FastAPI
from captchakit import CaptchaManager, ImageRenderer, MathChallengeFactory, MemoryStorage
from captchakit.adapters.fastapi import captcha_router, verify_captcha
manager = CaptchaManager(MathChallengeFactory(), ImageRenderer(), MemoryStorage())
app = FastAPI()
app.include_router(captcha_router(manager, prefix="/captcha"))
@app.post("/register")
async def register(_: None = Depends(verify_captcha(manager))) -> dict[str, bool]:
return {"ok": True}Run the bundled demo:
uv run python -m uvicorn examples.fastapi_login:app --reload
# open http://127.0.0.1:8000┌──────────────────────────────────────────────────────────────┐
│ Adapters (FastAPI / aiogram / Discord / Django) │
├──────────────────────────────────────────────────────────────┤
│ CaptchaManager (issue → render → persist · verify · TTL) │
├──────────────┬──────────────┬───────────────┬────────────────┤
│ Challenge │ Renderer │ Storage │ Rate limiter │
│ Text · Math │ Image · SVG │ Memory │ NoOp │
│ Grid · Word │ Audio │ Redis · PG │ Token bucket │
└──────────────┴──────────────┴───────────────┴────────────────┘
↓ i18n translator · metrics sink · clock
Everything coloured here is a Protocol — drop in your own implementation without subclassing.
- Constant-time comparison via
hmac.compare_digest. - Crypto-safe randomness via
secretsfor solution generation. - CPU-bound Pillow drawing offloaded to a worker thread with
asyncio.to_thread. - Multi-process safe when paired with
RedisStorageorPostgresStorage.
| Operation | Mean (ms) | Median (ms) | p99 (ms) |
|---|---|---|---|
ImageRenderer.render (PNG) |
3.64 | 3.54 | 4.84 |
SVGRenderer.render (SVG) |
0.03 | 0.03 | 0.05 |
AudioRenderer.render (WAV) |
2.69 | 2.60 | 3.72 |
issue + verify round-trip |
2.73 | 2.57 | 5.29 |
Measured on a single CPython 3.13 thread, Windows 10, 500 iterations after 20 warmups. Reproduce with uv run python benchmarks/bench.py.
- Run behind a proper reverse proxy with TLS and WAF rules.
- Use
RedisStorageorPostgresStorageif you scale beyond a single worker —MemoryStorageis per-process. - Wire
RateLimitertoRedisTokenBucket(or your edge WAF) when exposing the issue endpoint publicly. - Expose
PrometheusMetricson:9090/metricsand alert oncaptchakit_too_many_attempts_totalspikes. - Pair at least one visual renderer with
AudioRendererfor accessibility. - Set
ttlshort (60–180 s) andmax_attemptslow (2–3) — captchakit is a raise-the-cost layer, not a fortress.
captchakit is a lightweight human-check — it raises the cost for casual spam and scripted abuse. It is not a bot-farm-grade defence.
For high-value forms (payment, password reset, account takeover) use hCaptcha, Cloudflare Turnstile or reCAPTCHA Enterprise in addition to captchakit, and enforce rate limiting at the edge of your application.
Vulnerability reports: see SECURITY.md.
1.xis API-stable — public symbols follow semver. See docs/stability.md for the policy.- Supported Python versions: 3.10 → 3.13. New 3.x is added within one MINOR of upstream release.
- Every MINOR gets security patches throughout the life of the
1.xline.
Full docs at https://akerem16.github.io/captchakit/ — quickstart, adapter guides, storage & rate-limit recipes, i18n, metrics, accessibility, API reference.
captchakit ships first-class context files so coding agents (Cursor, Windsurf, Claude Code, Copilot Workspace, aider, …) can integrate it correctly on the first try:
llms.txt— concise, llmstxt.org-compatible index of every public symbol and the canonical recipes.AGENTS.md— deterministic, copy-paste task templates for common asks (“add a captcha to my FastAPI route”, “swap storage to Redis”, etc.), plus the API contract and pitfalls to avoid.
Drop either file into your agent's retrieval / context window and it should be able to wire captchakit into an app without hallucinating imports or inventing kwargs. Minimal hello-world an agent can output verbatim:
# Install: pip install "captchakit[fastapi]"
from fastapi import Depends, FastAPI
from captchakit import CaptchaManager, ImageRenderer, MathChallengeFactory, MemoryStorage
from captchakit.adapters.fastapi import captcha_router, verify_captcha
manager = CaptchaManager(MathChallengeFactory(), ImageRenderer(), MemoryStorage())
app = FastAPI()
app.include_router(captcha_router(manager, prefix="/captcha"))
@app.post("/signup")
async def signup(_: None = Depends(verify_captcha(manager))) -> dict[str, bool]:
return {"ok": True}Contributions welcome. Local development:
git clone https://github.com/akerem16/captchakit
cd captchakit
uv sync --all-extras
uv run ruff check .
uv run mypy
uv run pytest
uv run bandit -c pyproject.toml -r src
uv run pip-auditSee CONTRIBUTING.md.
MIT — see LICENSE.




