---
title: Python 3
icon: SiPython
---
Welcome to the Duckity Python SDK documentation! This guide will teach you how to install and set up
the SDK in no time.
The SDK is async-first. To call it from synchronous code, wrap the calls in `asyncio.run()` or your
event loop's version of it.
All SDKs are fully compatible with each other. Solution tokens generated in one SDK will be
validated just fine in other SDKs, including this one.
## Quick Start
Before you can integrate Duckity into your application, you'll need to have the following:
1. An application,
2. At least one protection profile created in that application, and
3. The ID of the protection profiles to use
If you're missing either of those, head over to the [Duckity Dashboard](https://app.duckity.com) or
read the [Quick Start](/quick-start) guide to learn how to set those up.
Once you got those ready, follow these steps to get things running on your client:
### Install the SDK [step]
Install the SDK from PyPI using your favorite package manager:
{/* prettier-ignore */}
```sh
pip install duckity
```
```sh
uv add duckity
```
```sh
poetry add duckity
```
### Solve a Challenge [step]
Solving a challenge only requires a protection profile ID.
```py
import duckity
solution: str = await duckity.solve(PROTECTION_PROFILE_ID)
```
The CPU-intensive part of solving the challenge is done in a `ProcessPoolExecutor()`.
Neither the GIL nor the async event loop will be blocked.
```py
import asyncio
import duckity
solution: str = asyncio.run(duckity.solve(PROTECTION_PROFILE_ID))
```
The CPU-intensive part of solving the challenge is done in a `ProcessPoolExecutor()`. The
GIL will not be blocked.
### Validate a Solution [step]
Validation is done server-side. You can send the solution token to your server any way you want; A
JSON field or an HTTP header is usually convenient.
To validate a solution token, you'll need 4 things:
1. The solution token,
2. The IP of the client that submitted the solution,
3. The application's secret, and
4. The protection profile's ID.
Once you have them, you can validate a solution token as follows:
```py
import duckity
solution: str
client_ip: str
application_secret: str
protection_profile_id: str
is_valid: bool = await duckity.validate(
solution, client_ip, application_secret, protection_profile_id
)
```
```py
import asyncio
import duckity
solution: str
client_ip: str
application_secret: str
protection_profile_id: str
is_valid: bool = asyncio.run(
duckity.validate(solution, client_ip, application_secret, protection_profile_id)
)
```
That's it! If the solution token is valid, you can proceed to process your request. If it's not,
return an error to the client and do not further process the request.
## Advanced Usage
Solving a challenge on demand works well for simple setups. However, UX can be greatly improved
changing a few settings and planning when to solve challenges.
### Asynchronous Challenge Solving
The challenge does not need to wait for the user to finish filling up a form or completing an action
to be issued. When you can guess the user will need a solution token, it is a good idea to start
computing it before the user needs it.
For example, if the user is logging in to a backend service via a CLI, you can fetch and solve a
challenge while the user is filling up their username and password. For example:
```py
import asyncio
import getpass
import duckity
async def login():
solution_task = asyncio.create_task(duckity.solve(PROTECTION_PROFILE_ID))
email = await asyncio.to_thread(input, "Enter your email: ")
password = await asyncio.to_thread(getpass.getpass, "Enter your password (hidden): ")
print("Logging you in...")
solution = await solution_task
# Log the user in here...
asyncio.run(login())
```
## Integrations
Duckity's Python SDK works anywhere out of the box. These examples show how to integrate it to some
common frameworks and tools.
### FastAPI
```py lineNumbers
import os
import dotenv
import duckity
from fastapi import FastAPI, Request, HTTPException
from pydantic import BaseModel
dotenv.load_dotenv(override=True)
PROTECTION_PROFILE_ID = os.environ["DUCKITY_PROTECTION_PROFILE_ID"]
APPLICATION_SECRET = os.environ["DUCKITY_APPLICATION_SECRET"]
app = FastAPI()
class RequestBodySchema(BaseModel):
solution: str
@app.post("/protected")
async def protected(request: Request, payload: RequestBodySchema):
is_valid = await duckity.validate(
payload.solution,
request.client.host, # Make sure to take into account X-Forwarded-For if behind a reverse proxy.
APPLICATION_SECRET,
PROTECTION_PROFILE_ID
)
if not is_valid:
raise HTTPException(
status_code=400,
detail="The provided solution token was invalid.",
)
return {
"message": "This is protected!"
}
```
### Flask
```py lineNumbers
import os
import dotenv
import duckity
from flask import Flask, request, jsonify
dotenv.load_dotenv(override=True)
app = Flask(__name__)
PROTECTION_PROFILE_ID = os.environ["DUCKITY_PROTECTION_PROFILE_ID"]
APPLICATION_SECRET = os.environ["DUCKITY_APPLICATION_SECRET"]
@app.post("/protected")
async def protected():
solution = request.json.get("solution")
if not solution:
return jsonify({"error": "Missing solution"}), 400
client_ip = request.remote_addr # Make sure to take into account X-Forwarded-For if behind a reverse proxy.
is_valid = await duckity.validate(
solution,
client_ip,
APPLICATION_SECRET,
PROTECTION_PROFILE_ID,
)
if not is_valid:
return jsonify({"error": "Invalid solution"}), 403
return jsonify({"message": "This is protected!"})
```
### Django
```py lineNumbers
import json
import duckity
from django.conf import settings
from django.http import JsonResponse
async def protected(request):
if request.method != "POST":
return JsonResponse(
{"detail": "Method not allowed."},
status=405,
)
try:
body = json.loads(request.body)
solution = body["solution"]
except (json.JSONDecodeError, KeyError, TypeError):
return JsonResponse(
{"detail": "Invalid request body."},
status=400,
)
# Make sure to take into account X-Forwarded-For if behind a reverse proxy.
client_ip = request.META["REMOTE_ADDR"]
is_valid = await duckity.validate(
solution,
client_ip,
settings.DUCKITY_APPLICATION_SECRET,
settings.DUCKITY_PROTECTION_PROFILE_ID,
)
if not is_valid:
return JsonResponse(
{"detail": "The provided solution token was invalid."},
status=400,
)
return JsonResponse({
"message": "This is protected!"
})
```
### Django Ninja
```py lineNumbers
from ninja import NinjaAPI, Schema
from ninja.errors import HttpError
from django.conf import settings
api = NinjaAPI()
class RequestBodySchema(Schema):
solution: str
@api.post("/protected")
async def protected(request, payload: RequestBodySchema):
# Make sure to take into account X-Forwarded-For if behind a reverse proxy.
client_ip = request.META["REMOTE_ADDR"]
is_valid = await duckity.validate(
payload.solution,
client_ip,
settings.DUCKITY_APPLICATION_SECRET,
settings.DUCKITY_PROTECTION_PROFILE_ID,
)
if not is_valid:
raise HttpError(400, "The provided solution token was invalid.")
return {
"message": "This is protected!"
}
```
### Django Rest Framework
```py lineNumbers
import duckity
from django.conf import settings
from rest_framework import serializers, status
from rest_framework.views import APIView
from rest_framework.response import Response
class RequestBodySerializer(serializers.Serializer):
solution = serializers.CharField()
class ProtectedView(APIView):
async def post(self, request):
serializer = RequestBodySerializer(data=request.data)
serializer.is_valid(raise_exception=True)
# Make sure to take into account X-Forwarded-For if behind a reverse proxy.
client_ip = request.META["REMOTE_ADDR"]
is_valid = await duckity.validate(
serializer.validated_data["solution"],
client_ip,
settings.DUCKITY_APPLICATION_SECRET,
settings.DUCKITY_PROTECTION_PROFILE_ID,
)
if not is_valid:
return Response(
{"detail": "The provided solution token was invalid."},
status=status.HTTP_400_BAD_REQUEST,
)
return Response({"message": "This is protected!"})
```
### Strawberry
```py lineNumbers
import os
import dotenv
import duckity
import strawberry
dotenv.load_dotenv(override=True)
PROTECTION_PROFILE_ID = os.environ["DUCKITY_PROTECTION_PROFILE_ID"]
APPLICATION_SECRET = os.environ["DUCKITY_APPLICATION_SECRET"]
@strawberry.type
class Query:
pass
@strawberry.type
class ProtectedResource:
message: str
@strawberry.type
class Mutation:
@strawberry.mutation
async def protected(self, info: strawberry.Info, solution: str) -> ProtectedResource:
# Make sure to take into account X-Forwarded-For if behind a reverse proxy.
client_ip = info.context["request"].META["REMOTE_ADDR"]
is_valid = await duckity.validate(
solution,
client_ip,
DUCKITY_APPLICATION_SECRET,
DUCKITY_PROTECTION_PROFILE_ID,
)
if not is_valid:
raise Exception("The provided solution was not valid.")
return ProtectedResource(message="This is protected!")
schema = strawberry.Schema(query=Query, mutation=Mutation)
```
## Contributing & License
All contributions are welcome to the SDK. Whether it's bug fixes, suggestions, new features,
documentation updates, or fixing a typo, if you think you can make this SDK better, feel free to
make a pull request in the [GitHub repository](https://github.com/duckity-com/sdks).
This SDK is licensed under the permissive MIT License, and so will be all contributions to the SDK.