Python GraphQL Libraries
Three actively-maintained options. Pick by paradigm preference. All work with FastAPI, Django, Flask, ASGI/WSGI.
The big three
| Library | Style | Best for |
|---|---|---|
| Strawberry | code-first, type annotations | modern Python, type-checker-friendly, FastAPI fans |
| Ariadne | schema-first, SDL files | polyglot teams sharing one schema |
| Graphene | code-first, class-based (older) | legacy projects |
graphql-core is the underlying engine all three use. You almost never use it directly.
For new projects: Strawberry. It’s the most idiomatic modern Python (Mapped-style annotations) and integrates cleanly with FastAPI/Starlette.
Strawberry — minimum example
# pip install "strawberry-graphql[fastapi]"
import strawberry
from strawberry.fastapi import GraphQLRouter
from fastapi import FastAPI
@strawberry.type
class User:
id: strawberry.ID
name: str
email: str
@strawberry.type
class Query:
@strawberry.field
def user(self, id: strawberry.ID) -> User | None:
# in real code: fetch from DB
return User(id=id, name="Alice", email="alice@example.com")
@strawberry.field
def users(self, limit: int = 20) -> list[User]:
return [User(id="1", name="Alice", email="a@b.com")]
schema = strawberry.Schema(query=Query)
app = FastAPI()
app.include_router(GraphQLRouter(schema), prefix="/graphql")
Run: uvicorn app:app. GraphiQL is at /graphql in dev.
Mutations:
@strawberry.input
class CreateUserInput:
name: str
email: str
@strawberry.type
class Mutation:
@strawberry.mutation
async def create_user(self, input: CreateUserInput) -> User:
# persist...
return User(id="42", name=input.name, email=input.email)
schema = strawberry.Schema(query=Query, mutation=Mutation)
Subscriptions:
import asyncio
from typing import AsyncIterator
@strawberry.type
class Subscription:
@strawberry.subscription
async def count(self, target: int = 10) -> AsyncIterator[int]:
for i in range(target):
yield i
await asyncio.sleep(1)
Strawberry — context
async def get_context(request: Request):
user = await get_current_user(request)
return {
"request": request,
"user": user,
"post_loader": DataLoader(load_posts),
}
app.include_router(
GraphQLRouter(schema, context_getter=get_context),
prefix="/graphql",
)
# In resolvers:
@strawberry.field
def my_posts(self, info: strawberry.Info) -> list[Post]:
user = info.context["user"]
return info.context["post_loader"].load(user.id)
The info argument exposes context, schema, the parent type, etc.
Strawberry — DataLoader
from strawberry.dataloader import DataLoader
async def load_users_batch(user_ids: list[int]) -> list[User]:
rows = await db.fetch_all("SELECT * FROM users WHERE id = ANY($1)", user_ids)
by_id = {r["id"]: User(**r) for r in rows}
return [by_id.get(uid) for uid in user_ids]
# In context_getter:
return {"user_loader": DataLoader(load_fn=load_users_batch)}
# In a resolver:
@strawberry.field
async def author(self, info) -> User | None:
return await info.context["user_loader"].load(self.author_id)
Strawberry — permissions
from strawberry.permission import BasePermission
class IsAuthenticated(BasePermission):
message = "Must be logged in"
async def has_permission(self, source, info, **kwargs) -> bool:
return info.context["user"] is not None
@strawberry.type
class Query:
@strawberry.field(permission_classes=[IsAuthenticated])
def me(self, info) -> User:
return info.context["user"]
Cleaner than checking inside resolvers. Compose multiple permission classes.
Ariadne — schema-first
# pip install ariadne ariadne[asgi]
from ariadne import make_executable_schema, QueryType, MutationType
from ariadne.asgi import GraphQL
type_defs = """
type User {
id: ID!
name: String!
email: String!
}
type Query {
user(id: ID!): User
users(limit: Int = 20): [User!]!
}
"""
query = QueryType()
@query.field("user")
async def resolve_user(_, info, id):
return await db.get_user(id)
@query.field("users")
async def resolve_users(_, info, limit):
return await db.list_users(limit)
schema = make_executable_schema(type_defs, query)
app = GraphQL(schema, debug=True)
The SDL is the source of truth; resolvers bind to schema types by name. Polyglot teams can share type_defs with Node.js / Go services.
Graphene — legacy class-based
Still maintained, but the API style predates Python typing.
import graphene
class User(graphene.ObjectType):
id = graphene.ID()
name = graphene.String()
email = graphene.String()
class Query(graphene.ObjectType):
user = graphene.Field(User, id=graphene.ID(required=True))
def resolve_user(self, info, id):
return User(id=id, name="Alice", email="a@b.com")
schema = graphene.Schema(query=Query)
Verbose for trivial models; no type checker support unless you decorate with annotations. For new projects, prefer Strawberry.
graphene-django
For Django apps with existing models. Auto-derives GraphQL types from Django models:
import graphene
from graphene_django import DjangoObjectType
from myapp.models import User
class UserType(DjangoObjectType):
class Meta:
model = User
fields = ("id", "name", "email")
class Query(graphene.ObjectType):
users = graphene.List(UserType)
def resolve_users(self, info):
return User.objects.all()
schema = graphene.Schema(query=Query)
Quick to start, less control. For more idiomatic Django GraphQL, strawberry-django is newer and cleaner.
strawberry-django
# pip install strawberry-graphql-django
import strawberry
import strawberry_django
from myapp.models import User as UserModel
@strawberry_django.type(UserModel)
class User:
id: strawberry.ID
name: str
email: str
@strawberry.type
class Query:
users: list[User] = strawberry_django.field()
Auto-generates resolvers and filtering. Strawberry’s typing benefits + Django’s ORM. The modern choice for Django + GraphQL.
Code generation for clients
For TypeScript / Swift / Kotlin clients, code generation from the schema gives you typed clients:
# graphql-codegen — Node.js tool, runs against your Python schema
npx graphql-codegen --config codegen.yml
# codegen.yml
schema: http://localhost:8000/graphql
documents: "src/**/*.graphql"
generates:
src/generated/types.ts:
plugins:
- typescript
- typescript-operations
Generates types for every operation, every fragment. Compile-time checking that client queries match the server schema.
Testing GraphQL
import pytest
from starlette.testclient import TestClient
@pytest.fixture
def client():
return TestClient(app)
def test_get_user(client):
response = client.post("/graphql", json={
"query": "query { user(id: \"1\") { name email } }"
})
assert response.status_code == 200
data = response.json()
assert data["data"]["user"]["name"] == "Alice"
assert "errors" not in data
For unit-testing schema execution without HTTP:
result = schema.execute_sync(
"query { user(id: \"1\") { name } }",
context_value={"user": current_user},
)
assert result.errors is None
assert result.data["user"]["name"] == "Alice"
execute_sync runs the executor directly; great for testing resolvers without spinning up the framework.
Schema export for tooling
# Strawberry: print SDL
print(schema.as_str())
# Run as a CLI command:
strawberry export-schema myapp.schema:schema > schema.graphql
The exported SDL is the contract — check it into git, use for code generation, diff in CI to catch breaking changes.
Monitoring and observability
| Concern | How |
|---|---|
| Error logging | hook into Schema execution to capture errors |
| Slow query detection | wrap executor with timing; log queries > threshold |
| Per-resolver metrics | resolver-level middleware (apollo-tracing for graphql-core) |
| Schema usage | track operation_name + fields used over time |
Apollo Studio / Inigo / Hasura Cloud are managed observability platforms. Self-hosted: Prometheus metrics from middleware + APM (Datadog, New Relic) with GraphQL plugins.
Common pitfalls
- No async resolvers when DB is async — sync-blocking resolvers in an async server stalls the event loop.
- Sharing DataLoaders across requests — leaks one user’s data to another.
- Mounting GraphQL at
/api/graphqlbut forgetting CORS / authentication middleware — public access. - Strawberry’s
infoconfused with FastAPIRequest—info.context["request"]gives the underlying request. - Using graphene for new code — works but verbose; Strawberry / strawberry-django is preferred.
Common interview confusions
- “Strawberry is for Django only.” — works with any ASGI framework: FastAPI, Starlette, Flask (via async support), Django.
- “Schema-first is the only ‘correct’ way.” — code-first is just as valid; better for single-language teams with strong typing.
- “You need Apollo Server to do GraphQL.” — Apollo is JavaScript. Python has Strawberry, Ariadne, Graphene.
Interview angle
- “What Python GraphQL library would you use today?” — Strawberry for new projects (code-first, type-annotation-based, integrates with FastAPI). Ariadne for schema-first teams. Graphene for legacy.
- “Code-first vs schema-first?” — code-first defines types in Python, generates SDL. Schema-first writes SDL, binds Python resolvers. Code-first benefits from type checkers (mypy/pyright); schema-first lets polyglot teams share one source of truth.
- “How do you wire DataLoader in Strawberry?” — create DataLoader instances in
context_getter(per-request); resolvers access viainfo.context["loader_name"].load(key). - “How would you test GraphQL endpoints?” —
schema.execute_sync(query, context_value=...)for unit tests (skips HTTP layer); HTTP TestClient against/graphqlfor integration tests. - “How do you generate typed clients from a Python GraphQL schema?” — export SDL (
strawberry export-schema), point a tool likegraphql-codegen(Node.js) at it. Generates TypeScript / Swift / Kotlin types for every operation. - “What’s strawberry-django?” — Strawberry’s Django integration: auto-derives GraphQL types from Django models, handles filtering and pagination. The modern alternative to graphene-django.