Async-first, fully type-hinted, minimal captcha library with adapters for aiogram, FastAPI and Discord.
Project description
captchakit
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.
Showcase
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.
Why captchakit
| 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 |
Install
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 adapter
30-second example
import 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())
FastAPI in 10 lines
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
Architecture
┌──────────────────────────────────────────────────────────────┐
│ 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.
Performance
| 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.
Production deployment
- 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.
Security scope
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.
Stability & compatibility
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.
Documentation
Full docs at https://akerem16.github.io/captchakit/ — quickstart, adapter guides, storage & rate-limit recipes, i18n, metrics, accessibility, API reference.
For AI agents / LLM tooling
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}
Contributing
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-audit
See CONTRIBUTING.md.
License
MIT — see LICENSE.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file captchakit-1.0.1.tar.gz.
File metadata
- Download URL: captchakit-1.0.1.tar.gz
- Upload date:
- Size: 32.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9f871b43a81a688adc6eadbea0a8df878d4774efdec0b60aba327a354eeecacf
|
|
| MD5 |
79f341fb1fe3c726d2493a3211c9fd15
|
|
| BLAKE2b-256 |
87960b3b2dbed185d9c21a6f3e8bc052f9bd8c0f2d93e17e29d6a0f6639f13ed
|
Provenance
The following attestation bundles were made for captchakit-1.0.1.tar.gz:
Publisher:
release.yml on akerem16/captchakit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
captchakit-1.0.1.tar.gz -
Subject digest:
9f871b43a81a688adc6eadbea0a8df878d4774efdec0b60aba327a354eeecacf - Sigstore transparency entry: 1342471097
- Sigstore integration time:
-
Permalink:
akerem16/captchakit@c93f7c8dcd80eb5b49b916e22764401b6c744693 -
Branch / Tag:
refs/tags/v1.0.1 - Owner: https://github.com/akerem16
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@c93f7c8dcd80eb5b49b916e22764401b6c744693 -
Trigger Event:
push
-
Statement type:
File details
Details for the file captchakit-1.0.1-py3-none-any.whl.
File metadata
- Download URL: captchakit-1.0.1-py3-none-any.whl
- Upload date:
- Size: 40.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3b629caabb7bf666dfe02e16a4662ca9b8b8f979c3654cafb1dfa12d976e1a91
|
|
| MD5 |
b5665d64d1540a7128f4ca3b35e91d08
|
|
| BLAKE2b-256 |
d27448624892dd165d0fcdca21b7e41fd6e4b1e2f1341fa91f3826303a4720a3
|
Provenance
The following attestation bundles were made for captchakit-1.0.1-py3-none-any.whl:
Publisher:
release.yml on akerem16/captchakit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
captchakit-1.0.1-py3-none-any.whl -
Subject digest:
3b629caabb7bf666dfe02e16a4662ca9b8b8f979c3654cafb1dfa12d976e1a91 - Sigstore transparency entry: 1342471115
- Sigstore integration time:
-
Permalink:
akerem16/captchakit@c93f7c8dcd80eb5b49b916e22764401b6c744693 -
Branch / Tag:
refs/tags/v1.0.1 - Owner: https://github.com/akerem16
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@c93f7c8dcd80eb5b49b916e22764401b6c744693 -
Trigger Event:
push
-
Statement type: