backend / protocols / graphql / 08_python_libraries.md

Python GraphQL Libraries

6 interview angles 6 min read source

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/graphql but forgetting CORS / authentication middleware — public access.
  • Strawberry’s info confused with FastAPI Requestinfo.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 via info.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 /graphql for integration tests.
  • “How do you generate typed clients from a Python GraphQL schema?” — export SDL (strawberry export-schema), point a tool like graphql-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.