Strawberry GraphQL Style Guide
crefft > Frameworks > Strawberry
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.
Extends Python style guide with Strawberry GraphQL-specific conventions.
Target Version: Strawberry 0.327.7 on Python 3.14.7
Quick Reference
All Python tooling applies. Additional considerations:
| Task | Tool | Command |
|---|---|---|
| Install | uv | uv add "strawberry-graphql[fastapi,cli]>=0.327.7,<0.328" |
| Run dev | Strawberry CLI | uv run strawberry dev schema |
| Test | pytest | pytest |
| Docs | Built-in | GraphiQL at /graphql |
Version Baseline
Projects MUST target Strawberry 0.327.71 on Python 3.14.72 and MUST declare a bounded version range instead of an open-ended floor:
# pyproject.toml
[project]
requires-python = ">=3.14"
dependencies = [
"strawberry-graphql[fastapi,cli]>=0.327.7,<0.328",
]
Projects MUST commit the resolved lock file (uv.lock) alongside pyproject.toml
so developer machines, CI, and production install identical versions.
Why: Strawberry publishes patch releases several times a week and five security
advisories have been filed against versions that an open-ended 0.287+ floor still
admits. A bounded range stops a resolver selecting an affected release, and the
committed lock file makes the installed version reproducible and auditable.
Python 3.14 is in bugfix support and reaches end of life in October 20303, so it remains a valid runtime target; 3.14.7 is the audited patch level.
Security Floor
Every advisory below is fixed at or before 0.327.7:
| Advisory | Affected versions | Fixed in | Impact |
|---|---|---|---|
| GHSA-vpwc-v33q-mq89 | <= 0.312.2 | 0.312.3 | Authentication bypass via the legacy graphql-ws WebSocket subprotocol |
| GHSA-hv3w-m4g2-5x77 | <= 0.312.2 | 0.312.3 | Denial of service via unbounded WebSocket subscriptions |
| GHSA-x97m-qp5c-w9xj | >= 0.288.4, <= 0.315.3 | 0.315.4 | Default GraphiQL may expose HTTP headers in URLs |
| GHSA-qfwv-87qj-98xq | >= 0.71.0, <= 0.315.6 | 0.315.7 | Circular fragment reference denial of service |
| GHSA-fr49-mhgj-crfc | >= 0.172.0, <= 0.315.6 | 0.315.7 | MaxAliasesLimiter bypass via fragment spreads |
Projects MUST NOT deploy Strawberry below 0.315.7, which is the lowest release that
carries all five advisory fixes. Projects MUST NOT deploy below 0.323.2 where any
operation may execute synchronously, because MaskErrors leaked parsing and validation
error text during synchronous execution until that release4.
Projects MUST apply Strawberry patch releases within one working week of publication and MUST audit the resolved dependency set in CI so a known-vulnerable version inside the pinned range fails the build:
uv export --frozen --no-emit-project --no-hashes -o requirements.txt
uvx pip-audit==2.10.1 --strict --no-deps --requirement requirements.txt
Why: The advertised support floor is what an operator reads as safe. 0.287+
promises support for releases carrying WebSocket authentication bypass, GraphiQL header
leakage, and the alias and fragment denial-of-service defects, while this guide
prescribes the very extensions and subscription endpoints those advisories target.
Bootstrapping a Project
The cli extra supplies the strawberry executable’s dependencies; the fastapi
extra alone raises MissingOptionalDependenciesError. The server subcommand has been
removed, so the dev server is started with strawberry dev:
# Don't: the FastAPI extra omits the CLI, and `server` is no longer a subcommand
uv add "strawberry-graphql[fastapi]"
strawberry server schema
# Do: install both extras at a bounded version and start the dev server
uv add "strawberry-graphql[fastapi,cli]>=0.327.7,<0.328"
uv run strawberry dev schema
strawberry dev schema imports the module named schema from the working directory
and serves the schema object defined in it, so the argument MUST match a module
that exposes a strawberry.Schema:
# schema.py
import strawberry
@strawberry.type
class Query:
@strawberry.field
def hello(self) -> str:
return "world"
schema = strawberry.Schema(query=Query)
Why: The dev server is the documented entry point for exploring a schema, so a
quick start that cannot install or start is worse than no quick start at all. Naming
the module explicitly avoids the common failure where strawberry dev cannot import
the argument it was given.
Why Strawberry?
Strawberry5 is a modern Python GraphQL library that uses Python’s native type hints and dataclasses to define schemas. Unlike schema-first approaches (like Ariadne) or verbose class-based definitions (like Graphene), Strawberry generates schemas directly from Python code.
Key advantages:
- Python-native schema definition using decorators and type hints
- Full async support for high-performance APIs
- Built-in Mypy plugin for static type checking
- First-class integrations with FastAPI, Django, and Flask
- DataLoader support for solving N+1 queries
Use Strawberry when building GraphQL APIs in Python, especially with FastAPI or Django. For REST APIs, see FastAPI or Django REST Framework.
Project Structure
Projects SHOULD organize Strawberry applications by domain:
my_app/
├── src/
│ └── myapp/
│ ├── __init__.py
│ ├── main.py # FastAPI app with GraphQL router
│ ├── config.py # Settings
│ ├── database.py # Database setup
│ ├── graphql/
│ │ ├── __init__.py
│ │ ├── schema.py # Root schema composition
│ │ ├── context.py # GraphQL context
│ │ ├── dataloaders.py # DataLoader definitions
│ │ ├── scalars.py # Custom scalar types
│ │ └── types/
│ │ ├── __init__.py
│ │ ├── user.py # User type + resolvers
│ │ └── order.py # Order type + resolvers
│ ├── models/
│ │ ├── __init__.py
│ │ └── user.py # SQLAlchemy models
│ └── services/
│ └── user.py # Business logic
├── tests/
│ ├── conftest.py
│ └── graphql/
│ └── test_users.py
├── pyproject.toml
└── .env
Why: Separating GraphQL types from database models and business logic maintains clean
boundaries. The graphql/types/ directory mirrors your domain, making navigation intuitive.
Schema Definition
Types
Projects MUST use @strawberry.type for GraphQL object types:
# src/myapp/graphql/types/user.py
import strawberry
from datetime import datetime
@strawberry.type
class User:
id: strawberry.ID
email: str
full_name: str
created_at: datetime
@strawberry.field
def display_name(self) -> str:
return self.full_name or self.email.split("@")[0]
Why: Strawberry leverages Python’s type system for schema generation. The decorator pattern is explicit and maintains Python’s readability while generating accurate GraphQL schemas.
Input Types
Projects MUST use @strawberry.input for mutation inputs:
@strawberry.input
class CreateUserInput:
email: str
password: str
full_name: str | None = None
@strawberry.input
class UpdateUserInput:
email: str | None = None
full_name: str | None = None
Why: Separate input types from output types. GraphQL requires this distinction, and it enables different validation rules for creation vs. updates.
Enums
Projects MUST use @strawberry.enum for enumeration types:
import strawberry
from enum import Enum
@strawberry.enum
class OrderStatus(Enum):
PENDING = "pending"
PROCESSING = "processing"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"
Resolvers
Async Resolvers
Projects MUST use async def for resolvers that perform I/O:
# src/myapp/graphql/types/user.py
import strawberry
from strawberry.types import Info
from myapp.graphql.context import Context
from myapp.models.user import User as UserModel
@strawberry.type
class Query:
@strawberry.field
async def user(self, info: Info[Context, None], id: strawberry.ID) -> User | None:
db = info.context.db
result = await db.execute(select(UserModel).where(UserModel.id == int(id)))
user_model = result.scalar_one_or_none()
if user_model:
return User.from_model(user_model)
return None
@strawberry.field
async def users(self, info: Info[Context, None]) -> list[User]:
db = info.context.db
result = await db.execute(select(UserModel))
return [User.from_model(u) for u in result.scalars()]
Why: Strawberry documentation emphasizes: “It is recommended to use async def for all fields
if handling concurrent requests on a single worker.” Synchronous functions block the entire worker
despite FastAPI’s threadpool.
Field Arguments
Projects SHOULD use type hints for field arguments:
@strawberry.type
class Query:
@strawberry.field
async def users(
self,
info: Info[Context, None],
limit: int = 10,
offset: int = 0,
status: OrderStatus | None = None,
) -> list[User]:
query = select(UserModel)
if status:
query = query.where(UserModel.status == status.value)
query = query.limit(limit).offset(offset)
result = await info.context.db.execute(query)
return [User.from_model(u) for u in result.scalars()]
Mutations
Projects MUST organize mutations with clear naming and input types:
# src/myapp/graphql/types/user.py
@strawberry.type
class Mutation:
@strawberry.mutation
async def create_user(
self,
info: Info[Context, None],
input: CreateUserInput,
) -> User:
service = UserService(info.context.db)
user_model = await service.create_user(
email=input.email,
password=input.password,
full_name=input.full_name,
)
return User.from_model(user_model)
@strawberry.mutation
async def update_user(
self,
info: Info[Context, None],
id: strawberry.ID,
input: UpdateUserInput,
) -> User:
service = UserService(info.context.db)
user_model = await service.update_user(
user_id=int(id),
**{k: v for k, v in input.__dict__.items() if v is not None},
)
return User.from_model(user_model)
@strawberry.mutation
async def delete_user(self, info: Info[Context, None], id: strawberry.ID) -> bool:
service = UserService(info.context.db)
return await service.delete_user(int(id))
Why: Use input types rather than multiple arguments for mutations with more than 2-3 parameters. This improves readability and makes schema evolution easier.
DataLoaders
Projects MUST use DataLoaders to solve the N+1 query problem:
# src/myapp/graphql/dataloaders.py
from collections.abc import Sequence
from strawberry.dataloader import DataLoader
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from myapp.models.user import User as UserModel
from myapp.models.order import Order as OrderModel
async def load_users(keys: Sequence[int], db: AsyncSession) -> list[UserModel | None]:
result = await db.execute(select(UserModel).where(UserModel.id.in_(keys)))
users_by_id = {u.id: u for u in result.scalars()}
return [users_by_id.get(key) for key in keys]
async def load_orders_by_user(
keys: Sequence[int], db: AsyncSession
) -> list[list[OrderModel]]:
result = await db.execute(select(OrderModel).where(OrderModel.user_id.in_(keys)))
orders_by_user: dict[int, list[OrderModel]] = {key: [] for key in keys}
for order in result.scalars():
orders_by_user[order.user_id].append(order)
return [orders_by_user[key] for key in keys]
def create_dataloaders(db: AsyncSession) -> dict[str, DataLoader]:
return {
"users": DataLoader(load_fn=lambda keys: load_users(keys, db)),
"orders_by_user": DataLoader(load_fn=lambda keys: load_orders_by_user(keys, db)),
}
# Using DataLoaders in resolvers
@strawberry.type
class User:
id: strawberry.ID
email: str
@strawberry.field
async def orders(self, info: Info[Context, None]) -> list[Order]:
orders = await info.context.dataloaders["orders_by_user"].load(int(self.id))
return [Order.from_model(o) for o in orders]
Why: DataLoaders batch multiple individual loads into a single query. Without them, fetching 10 users with their orders would be 11 queries (1 + N). With DataLoaders, it’s 2 queries.
DataLoader Best Practices
- Create loaders per-request: Instantiate DataLoaders in context to prevent cross-request cache pollution
- Clear cache after mutations: Use
loader.clear(key)after modifying data - Prime cache when available: Use
loader.prime(key, value)when data is already loaded
Context
Projects MUST use a typed context for dependency injection:
# src/myapp/graphql/context.py
from dataclasses import dataclass
from strawberry.fastapi import BaseContext
from sqlalchemy.ext.asyncio import AsyncSession
from strawberry.dataloader import DataLoader
from myapp.models.user import User
@dataclass
class Context(BaseContext):
db: AsyncSession
dataloaders: dict[str, DataLoader]
current_user: User | None = None
# src/myapp/main.py
from fastapi import FastAPI, Depends
from strawberry.fastapi import GraphQLRouter
from myapp.graphql.schema import schema
from myapp.graphql.context import Context
from myapp.graphql.dataloaders import create_dataloaders
from myapp.database import get_db
from myapp.auth import get_current_user
async def get_context(
db: AsyncSession = Depends(get_db),
current_user: User | None = Depends(get_current_user),
) -> Context:
return Context(
db=db,
dataloaders=create_dataloaders(db),
current_user=current_user,
)
graphql_app = GraphQLRouter(schema, context_getter=get_context)
app = FastAPI()
app.include_router(graphql_app, prefix="/graphql")
Why: Typed context enables IDE autocompletion and type checking. Creating DataLoaders and database sessions per-request ensures proper resource cleanup and cache isolation.
Permissions
Projects MUST use Strawberry’s permission system for authorization:
# src/myapp/graphql/permissions.py
from strawberry.permission import BasePermission
from strawberry.types import Info
from myapp.graphql.context import Context
class IsAuthenticated(BasePermission):
message = "User is not authenticated"
def has_permission(self, source: object, info: Info[Context, None], **kwargs) -> bool:
return info.context.current_user is not None
class IsAdmin(BasePermission):
message = "User is not an admin"
def has_permission(self, source: object, info: Info[Context, None], **kwargs) -> bool:
user = info.context.current_user
return user is not None and user.is_admin
class IsOwner(BasePermission):
message = "User does not own this resource"
def has_permission(self, source: object, info: Info[Context, None], **kwargs) -> bool:
user = info.context.current_user
if user is None:
return False
# Check ownership based on the source object
if hasattr(source, "user_id"):
return source.user_id == user.id
return False
# Applying permissions
@strawberry.type
class Mutation:
@strawberry.mutation(permission_classes=[IsAuthenticated])
async def create_order(self, info: Info[Context, None], input: CreateOrderInput) -> Order:
...
@strawberry.mutation(permission_classes=[IsAdmin])
async def delete_user(self, info: Info[Context, None], id: strawberry.ID) -> bool:
...
Why: Declarative permissions keep authorization logic separate from business logic. Permission classes are reusable and testable in isolation.
Schema Composition
Projects SHOULD compose schemas from domain-specific types:
# src/myapp/graphql/schema.py
import strawberry
from strawberry.extensions import QueryDepthLimiter, MaxTokensLimiter
from myapp.graphql.types.user import Query as UserQuery, Mutation as UserMutation
from myapp.graphql.types.order import Query as OrderQuery, Mutation as OrderMutation
@strawberry.type
class Query(UserQuery, OrderQuery):
pass
@strawberry.type
class Mutation(UserMutation, OrderMutation):
pass
schema = strawberry.Schema(
query=Query,
mutation=Mutation,
extensions=[
lambda: QueryDepthLimiter(max_depth=10),
lambda: MaxTokensLimiter(max_token_count=1000),
],
)
Why: Multiple inheritance for Query/Mutation types enables domain-driven organization while producing a single coherent schema.
Security Extensions
Projects MUST configure security extensions for production:
from strawberry.extensions import (
QueryDepthLimiter,
MaxTokensLimiter,
MaxAliasesLimiter,
MaskErrors,
)
schema = strawberry.Schema(
query=Query,
mutation=Mutation,
extensions=[
lambda: QueryDepthLimiter(max_depth=10),
lambda: MaxTokensLimiter(max_token_count=1000),
lambda: MaxAliasesLimiter(max_alias_count=15),
MaskErrors, # Hide internal error details in production
],
)
| Extension | Purpose | Recommended Value |
|---|---|---|
QueryDepthLimiter | Prevent deeply nested queries | 10-15 |
MaxTokensLimiter | Limit query complexity | 1000-2000 |
MaxAliasesLimiter | Prevent alias abuse | 10-20 |
MaskErrors | Hide sensitive error info | Enable in production |
Why: GraphQL’s flexibility can be exploited for denial-of-service attacks. These extensions prevent abuse while allowing legitimate queries.
Extension Behavior Depends on the Version
These extensions MUST NOT be treated as effective on their own, because three of
them were themselves defective in releases the old 0.287+ floor admitted:
MaxAliasesLimitercounted aliases statically and so could be bypassed by fragment spread amplification up to 0.315.6 (GHSA-fr49-mhgj-crfc).QueryDepthLimiterrecursed without cycle detection, so circular fragment references crashed validation with aRecursionErrorup to 0.315.6 (GHSA-qfwv-87qj-98xq).MaskErrorsleaked parsing and validation error text during synchronous execution up to 0.323.14.
Projects MUST pass extension classes or zero-argument factories to
Schema(extensions=...) rather than instances. From 0.316.0 an instance is deprecated,
is rejected by the extensions type signature, and is reused for every request, so
concurrent requests can observe each other’s ExecutionContext6.
# Don't: one shared instance per schema, and a DeprecationWarning from 0.316.0
extensions=[QueryDepthLimiter(max_depth=10), MaskErrors()]
# Do: a fresh extension per request
extensions=[lambda: QueryDepthLimiter(max_depth=10), MaskErrors]
Projects MUST cover the masking behavior with a test that pushes a malformed and an invalid operation through the synchronous entry point:
# tests/graphql/test_masking.py
import pytest
from myapp.graphql.schema import schema
@pytest.mark.parametrize(
"query",
["{ users ", "{ notAField }"],
ids=["malformed", "unknown-field"],
)
def test_sync_execution_masks_pre_execution_errors(query: str) -> None:
result = schema.execute_sync(query)
assert result.errors is not None
assert [error.message for error in result.errors] == ["Unexpected error."]
Why: execute_sync takes a different code path from execute, so masking has to be
asserted separately. On 0.323.1 the same assertions return
Syntax Error: Expected Name, found <EOF>. and Cannot query field 'notAField' on type 'Query'., handing an attacker the schema shape. The test pins the fixed behavior so a
downgrade inside the pinned range fails CI.
Disabling Introspection
Projects SHOULD disable introspection in production:
from strawberry.extensions import DisableIntrospection
from myapp.config import settings
extensions = [
lambda: QueryDepthLimiter(max_depth=10),
]
if not settings.DEBUG:
extensions.append(DisableIntrospection)
schema = strawberry.Schema(query=Query, mutation=Mutation, extensions=extensions)
FastAPI Integration
Projects using FastAPI MUST follow this integration pattern:
# src/myapp/main.py
from fastapi import FastAPI
from strawberry.fastapi import GraphQLRouter
from myapp.graphql.schema import schema
from myapp.graphql.context import get_context
from myapp.config import settings
def create_app() -> FastAPI:
app = FastAPI(title=settings.PROJECT_NAME)
graphql_app = GraphQLRouter(
schema,
context_getter=get_context,
graphql_ide="graphiql" if settings.DEBUG else None,
allow_queries_via_get=False, # Disable GET queries for security
)
app.include_router(graphql_app, prefix="/graphql")
return app
app = create_app()
Django Integration
Projects using Django SHOULD use Strawberry’s Django integration:
# myapp/schema.py
import strawberry
from strawberry_django import type as django_type
from myapp.models import User as UserModel
@django_type(UserModel)
class User:
id: strawberry.auto
email: strawberry.auto
full_name: strawberry.auto
# myapp/urls.py
from django.urls import path
from strawberry.django.views import AsyncGraphQLView
from myapp.schema import schema
urlpatterns = [
path("graphql/", AsyncGraphQLView.as_view(schema=schema)),
]
Type Checking with Mypy
Projects MUST enable Strawberry’s Mypy plugin:
# pyproject.toml
[tool.mypy]
plugins = ["strawberry.ext.mypy_plugin"]
Why: The Mypy plugin validates that GraphQL types match their Python definitions, catching type mismatches before runtime.
Testing
Projects MUST test GraphQL operations using the schema directly:
# tests/conftest.py
import pytest
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from myapp.graphql.schema import schema
from myapp.graphql.context import Context
from myapp.graphql.dataloaders import create_dataloaders
from myapp.database import Base
@pytest.fixture
async def db_session():
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
async_session = async_sessionmaker(engine, expire_on_commit=False)
async with async_session() as session:
yield session
@pytest.fixture
def context(db_session: AsyncSession) -> Context:
return Context(
db=db_session,
dataloaders=create_dataloaders(db_session),
current_user=None,
)
# tests/graphql/test_users.py
import pytest
from myapp.graphql.schema import schema
@pytest.mark.asyncio
async def test_create_user(context) -> None:
query = """
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
email
fullName
}
}
"""
result = await schema.execute(
query,
variable_values={
"input": {
"email": "test@example.com",
"password": "password123",
"fullName": "Test User",
}
},
context_value=context,
)
assert result.errors is None
assert result.data["createUser"]["email"] == "test@example.com"
@pytest.mark.asyncio
async def test_get_users(context, user_factory) -> None:
await user_factory.create_batch(3)
query = """
query GetUsers {
users {
id
email
}
}
"""
result = await schema.execute(query, context_value=context)
assert result.errors is None
assert len(result.data["users"]) == 3
Why: Testing against the schema directly (rather than HTTP) isolates GraphQL logic from transport concerns. Pass variables dynamically rather than hardcoding them in query strings.
Testing Permissions
@pytest.mark.asyncio
async def test_create_order_requires_auth(context) -> None:
query = """
mutation CreateOrder($input: CreateOrderInput!) {
createOrder(input: $input) {
id
}
}
"""
result = await schema.execute(
query,
variable_values={"input": {"items": []}},
context_value=context, # No current_user
)
assert result.errors is not None
assert "not authenticated" in result.errors[0].message.lower()
Error Handling
Projects SHOULD use custom error types for domain errors:
# src/myapp/graphql/errors.py
from typing import Annotated
import strawberry
from myapp.graphql.types.user import User
@strawberry.type
class ValidationError:
field: str
message: str
@strawberry.type
class NotFoundError:
message: str = "Resource not found"
# Union type for mutation responses
UserResult = Annotated[
User | ValidationError | NotFoundError,
strawberry.union("UserResult"),
]
Projects MUST declare unions with typing.Annotated. The types keyword argument
to strawberry.union() was deprecated in 0.191.0 and removed in 0.298.0, where it now
raises TypeError: union() got an unexpected keyword argument 'types' at import
time7.
# Don't: removed in 0.298.0, fails when the module is imported
UserResult = strawberry.union("UserResult", types=[User, ValidationError, NotFoundError])
# Do: declare the members in the annotation
UserResult = Annotated[User | ValidationError | NotFoundError, strawberry.union("UserResult")]
Projects migrating from the removed form SHOULD run Strawberry’s codemod rather than editing declarations by hand:
uv run strawberry upgrade annotated-union src/myapp
Why: The union alias is evaluated while the module is imported, so a removed
argument breaks the whole schema rather than a single field. The codemod rewrites every
call site and adds the Annotated import, which avoids missed declarations in large
schemas.
@strawberry.type
class Mutation:
@strawberry.mutation
async def create_user(
self, info: Info[Context, None], input: CreateUserInput
) -> UserResult:
# Check for existing user
existing = await info.context.db.execute(
select(UserModel).where(UserModel.email == input.email)
)
if existing.scalar_one_or_none():
return ValidationError(field="email", message="Email already registered")
user = await create_user_service(info.context.db, input)
return User.from_model(user)
Why: Union types for results enable type-safe error handling in clients. This follows the “errors as data” pattern preferred in GraphQL over throwing exceptions.
Subscriptions
Projects MAY implement subscriptions for real-time updates:
import asyncio
from collections.abc import AsyncGenerator
import strawberry
from strawberry.types import Info
from myapp.graphql.context import Context
@strawberry.type
class Subscription:
@strawberry.subscription
async def order_status_changed(
self, info: Info[Context, None], order_id: strawberry.ID
) -> AsyncGenerator[Order, None]:
pubsub = info.context.pubsub
async for message in pubsub.subscribe(f"order:{order_id}"):
yield Order.from_dict(message)
For production subscriptions, use Redis pub/sub or similar message brokers.
Projects exposing subscriptions MUST run at least 0.312.3, which fixes an
authentication bypass via the legacy graphql-ws WebSocket subprotocol
(GHSA-vpwc-v33q-mq89) and a denial of service from unbounded WebSocket
subscriptions (GHSA-hv3w-m4g2-5x77). The 0.327.7 baseline carries both
fixes.
Why: The legacy graphql-ws handler did not require a completed connection_init
handshake before processing subscription messages, so a client could skip the
on_ws_connect authentication hook entirely. Both subprotocols are enabled by default
in every integration that supports WebSockets and the client selects one through the
Sec-WebSocket-Protocol header, so the endpoint is only as safe as the installed
version.
Pre-commit Configuration
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.16.6
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v2.3.1
hooks:
- id: mypy
additional_dependencies: [strawberry-graphql]
args: [--strict]
See Also
- Python Style Guide - Language-level Python conventions
- FastAPI Style Guide - FastAPI-specific patterns
- Django Style Guide - Django-specific patterns
- GraphQL Best Practices - General GraphQL design patterns
References
Footnotes
-
strawberry-graphql 0.327.7 on PyPI - Audited baseline release ↩
-
Python 3.14.7 - Current 3.14 maintenance release ↩
-
Status of Python versions - Python 3.14 support window ↩
-
Strawberry 0.323.2 release notes - Synchronous
MaskErrorsfix ↩ ↩2 -
Strawberry GraphQL - Python GraphQL library ↩
-
Strawberry 0.316.0 release notes - Per-request extension construction ↩
-
Strawberry 0.298.0 release notes - Removal of the
typesunion argument ↩