iron_gql is a lightweight GraphQL code generator and runtime that turns schema SDL and real query documents into typed Python clients powered by Pydantic models. Use it to wire GraphQL APIs into services, CLIs, background jobs, or tests without hand-writing boilerplate.
pip install iron-gql # runtime only (httpx + pydantic)
pip install iron-gql[codegen] # + graphql-core for code generation- Query discovery.
generate_gql_packagescans your codebase for calls that look like<package>_gql("""..."""), validates each statement, and emits a module with strongly typed helpers. - Typed inputs and results. Generated Pydantic models mirror every selection set, enum, and input object referenced by the discovered queries.
- Async runtime.
runtime.GQLClientspeaks to GraphQL endpoints overhttpxand can shortcut network hops when pointed at an ASGI app. - Deterministic validation.
graphql-core(codegen dependency) enforces schema compatibility and rejects duplicate operation names with incompatible bodies.
runtime.py– provides the asyncGQLClient, the reusableGQLQuerybase class, and value serialization helpers.codegen/generator.py– orchestrates query discovery, validation, and module rendering.codegen/parser.py– converts GraphQL AST into typed helper structures consumed by the renderer.
- Describe your schema. Point
generate_gql_packageat an SDL file (schema.graphql). Include whichever root types you rely on (query, mutation, subscription). - Author queries where they live. Import the future helper and wrap your GraphQL statement:
The generator infers the helper name (
from myapp.gql.client import client_gql get_user = client_gql( """ query GetUser($id: ID!) { user(id: $id) { id name } } """ )
client_gql) from the package path you ask it to build. - Generate the client module.
The call writes
from pathlib import Path from iron_gql.codegen import generate_gql_package generate_gql_package( schema_path=Path("schema.graphql"), package_full_name="myapp.gql.client", base_url_import="myapp.config:GRAPHQL_URL", scalars={"ID": "builtins:str"}, to_camel_fn_full_name="myapp.inflection:to_camel", to_snake_fn=my_project_to_snake, debug_path=Path("iron_gql/debug/myapp.gql.client"), src_path=Path("."), )
myapp/gql/client.pycontaining:- an async client singleton,
- Pydantic result and input models,
- a query class per operation with typed
executemethods, - overloads for the helper function so editors can infer return types.
- Call your API.
async def fetch_user(user_id: str): query = get_user.with_headers({"Authorization": "Bearer token"}) result = await query.execute(id=user_id) return result.user
The generator maps GraphQL scalars to Python types in two layers:
Built-in scalars are mapped automatically:
| GraphQL | Python |
|---|---|
String, Int, Float, Boolean |
str, int, float, bool |
Date |
datetime.date |
DateTime |
datetime.datetime |
JSON |
object |
Upload |
iron_gql.FileVar |
Custom scalars are configured via the scalars parameter in "module:type" format:
generate_gql_package(
...,
scalars={
"ID": "builtins:str",
"Money": "decimal:Decimal",
"ULID": "ulid:ULID",
},
)Custom scalar types must be Pydantic-compatible — i.e. Pydantic should know how to parse them from JSON (deserialization) and serialize them to JSON. This works out of the box for standard library types (datetime, Decimal, UUID, Enum) and for any type that implements __get_pydantic_core_schema__. Unknown scalars fall back to object with a log warning.
- Naming conventions. Supply
to_camel_fn_full_name(module:path string) and ato_snake_fncallable to align casing with your ownalias_generator. - Endpoint configuration.
base_url_importis written verbatim into the generated module; point it at a global string, config object, or helper that returns the GraphQL endpoint.
GQLClientaccepts ASGItarget_appso you can reuse the runtime for production HTTP calls or in-process ASGI execution.GQLQuery.with_headersandGQLQuery.with_file_uploadsclone the query object, making per-call customization trivial.Uploadscalars map toiron_gql.FileVarfor multipart file handling.serialize_varconverts variables to JSON-friendly structures via Pydantic'sTypeAdapter, supporting custom scalar types alongside nested models, dicts, and lists.
The example/ directory contains a complete working setup: a GraphQL schema with queries, mutations, enums, interfaces, unions, and fragments, plus the generation script and sample query definitions. See example/generate.py for the codegen call and example/myapp/main.py for query usage.
Override the generated client via monkeypatch (or any other module attribute patching) to point queries at a test server or an ASGI app:
from iron_gql import runtime
from myapp.gql import api
async def test_get_user(monkeypatch):
test_client = runtime.GQLClient(
base_url="http://testserver",
target_app=my_asgi_app,
)
monkeypatch.setattr(api, "API_CLIENT", test_client)
result = await get_user.execute(id="1")
assert result.user.name == "Alice"The generated query classes resolve the client by module attribute name at call time, so replacing it is sufficient. The attribute is always named {PACKAGE}_CLIENT — for a package myapp.gql.api it is API_CLIENT.
- Errors identify the file and line where the problematic statement lives.
- Duplicate operation names must share identical bodies; rename or consolidate to resolve the conflict.