graphql-core review
GraphQL-core 3.2.11 is the Python implementation beneath Graphene, Ariadne, Strawberry, and other GraphQL server layers. Direct users get the parser, AST visitor, schema objects, standard validation rules, query executor, subscription engine, introspection helpers, and `GraphQLError` model. HTTP routing, WebSocket protocol handling, DataLoader-style batching, and decorator-based schema authoring are outside the package. This release tracks GraphQL.js 16.14.1, supports Python 3.7 through 3.14, makes `ofType` introspection depth configurable, permits directives on directive definitions, and restores an own-property check during AST value coercion.
GraphQL-core 3.2.11 installed as 1 package and 2 MB in 0.2 seconds on our sandbox, giving Python code direct access to the GraphQL.js 16.14.1 execution model. Use it for GraphQL infrastructure or low-level control; a normal API team will write less glue with Strawberry or Ariadne on top.
We installed it
| Install | ✓ · 0.2s | 1 package on disk · 2 MB |
| Import | ✓ | import graphql in 0.71s · pure Python · py.typed · requires Python >=3.7,<4 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does graphql-core install cleanly?
Yes. In a fresh container with an empty cache, pip install graphql-core finished in 0.2s, leaving 1 package and 2 MB on disk. pip-audit reported no known vulnerabilities.
What does graphql-core need to run?
Python >=3.7,<4, and nothing compiled: it is pure Python. In our run import graphql succeeded in 0.71s, and the package ships py.typed for type checkers.
graphql-core or strawberry-graphql: which should you use?
strawberry-graphql: Use it for annotation-driven schemas and maintained ASGI or framework adapters. GraphQL-core 3.2.11 installed as 1 package and 2 MB in 0.2 seconds on our sandbox, giving Python code direct access to the GraphQL.js 16.14.1 execution model.
When should you not use graphql-core?
The goal is an ordinary GraphQL API with an ASGI or Django endpoint. Strawberry, Ariadne, and Graphene supply the missing schema and web integration while depending on this engine.
Use it if
- You are writing a query analyzer, persisted-query service, custom validation rule, schema tool, or another layer that needs the GraphQL machinery directly.
- A framework abstraction is hiding resolver context, execution middleware, AST nodes, or schema details that your service must control.
- Python behavior should stay close to the GraphQL.js reference implementation for parsing, coercion, validation, and execution.
- The application needs to construct `GraphQLSchema` objects or consume `subscribe()` results without adopting a schema framework.
- The goal is an ordinary GraphQL API with an ASGI or Django endpoint. Strawberry, Ariadne, and Graphene supply the missing schema and web integration while depending on this engine.
- Your dependency policy assumes minor releases are backward compatible. The maintainer maps GraphQL.js majors to GraphQL-core minors and promises compatibility only across patches.
- Resolver batching must come in the box. GraphQL-core has no DataLoader, so the application or an upper layer must prevent N+1 database queries.
- Asynchronous execution has to run through Trio, gevent, or a custom executor model. Version 3 uses asyncio for coroutine resolvers.
- You expect `subscribe()` to manage a WebSocket connection. It creates the event stream; a server adapter must implement negotiation, authentication, and connection cleanup.
Setup reality
We installed GraphQL-core 3.2.11 in a clean Python 3.12 Bookworm container. The job took 0.2 seconds and left 1 package using 2 MB. pip-audit found 0 known vulnerabilities, and import graphql completed in 0.71 seconds. It is pure Python, declares 1 direct dependency, includes py.typed, and accepts Python 3.7 or newer below Python 4. No compiler, account, service, or configuration file was involved.
Pin the 3.2 line deliberately. GraphQL-core does not copy GraphQL.js version numbers: a GraphQL.js major becomes a GraphQL-core minor, so 3.3 can break code that works on 3.2. The README recommends ~=3.2.0. Release 3.2.11 corresponds to GraphQL.js 16.14.1, while the 3.3 release candidate follows GraphQL.js 17 prereleases and starts at Python 3.10. Check a framework's constraint before forcing a newer transitive version.
Direct schema construction is explicit. Fields use GraphQLField, arguments use GraphQLArgument, and SDL-built fields still need Python resolvers attached. A Python resolver receives parent and GraphQLResolveInfo positionally, then GraphQL field arguments as keywords. JavaScript examples use a different calling convention. Put per-request database sessions and identity on info.context. Call graphql_sync only when every resolver is synchronous; use and await graphql when a resolver can return a coroutine.
The convenience entry points parse, validate, and execute on each call. Persisted-query systems can cache a parsed document after validation, then call execute, but a schema revision must invalidate that cache. subscribe() yields an async iterator after successful setup and an ExecutionResult on setup error. Resolver exceptions become GraphQLError instances and retain original_error for logs. Filter client-facing error messages because exception text may contain internal data.
Patterns
Build an SDL schema and attach a resolver build-schema-sdl
from graphql import build_schema
schema = build_schema("""
type Query { greeting(name: String!): String! }
""")
def greeting(_parent, _info, name: str) -> str:
return f"Hello, {name}"
schema.query_type.fields["greeting"].resolve = greeting`build_schema` creates types and fields but does not discover Python functions; each application resolver still needs binding.
Declare schema objects directly construct-object-schema
from graphql import GraphQLField, GraphQLObjectType, GraphQLSchema, GraphQLString
query = GraphQLObjectType(
"Query",
{"health": GraphQLField(GraphQLString, resolve=lambda _p, _i: "ok")},
)
schema = GraphQLSchema(query=query)Direct construction requires `GraphQLField` objects rather than plain resolver callables.
Execute a synchronous query run-sync-query
from graphql import graphql_sync
result = graphql_sync(schema, '{ greeting(name: "Ada") }')
if result.errors:
raise ValueError([error.message for error in result.errors])
print(result.data)`graphql_sync` parses and validates before execution, and every resolver in that path must return a non-awaitable value.
Execute coroutine resolvers run-async-query
from graphql import graphql
async def account(_parent, info, id: str):
return await info.context["accounts"].get(id)
schema.query_type.fields["account"].resolve = account
result = await graphql(schema, source, context_value={"accounts": store})Request services passed through `context_value` appear on `info.context`; await `graphql` when any resolver may return a coroutine.
Parse, validate, then execute separate-execution-stages
from graphql import execute, parse, validate
document = parse(source)
errors = validate(schema, document)
if errors:
raise ValueError([item.message for item in errors])
result = execute(schema, document, context_value=context)Cached documents must be revalidated when the schema changes; otherwise removed fields or rules can slip past the cache.
Add a field-level validation rule define-validation-rule
from graphql import GraphQLError, ValidationRule, parse, specified_rules, validate
class RejectSecret(ValidationRule):
def enter_field(self, node, *_args):
if node.name.value == "secret":
self.report_error(GraphQLError("Field unavailable", node))
errors = validate(schema, parse(source), (*specified_rules, RejectSecret))Keep `specified_rules` in the tuple. Passing only the custom class disables the standard GraphQL validation set.
Reject schema introspection fields disable-introspection
from graphql import NoSchemaIntrospectionCustomRule, parse, specified_rules, validate
rules = (*specified_rules, NoSchemaIntrospectionCustomRule)
errors = validate(schema, parse(source), rules)This custom rule blocks `__schema` and `__type` but keeps `__typename`; it does not replace authorization checks on ordinary fields.
Collect selected field names walk-query-ast
from graphql import Visitor, parse, visit
class Fields(Visitor):
def __init__(self):
super().__init__()
self.names = []
def enter_field(self, node, *_args):
self.names.append(node.name.value)
collector = Fields()
visit(parse(source), collector)Visitor callbacks use snake-case names such as `enter_field`, and subclasses must call the base initializer.
Return GraphQL-shaped data and errors format-errors
result = graphql_sync(schema, source, context_value=context)
payload = result.formatted
for error in result.errors or []:
logger.error("resolver error", exc_info=error.original_error)`formatted` is suitable for the response shape, but raw exception messages may expose internal details and should be filtered by the server.
Recreate a schema from introspection build-client-schema
from graphql import build_client_schema, get_introspection_query, graphql_sync
result = graphql_sync(schema, get_introspection_query())
if result.errors:
raise RuntimeError(result.errors)
client_schema = build_client_schema(result.data)An introspected client schema contains type information but no executable field resolvers.
Apply an SDL extension extend-schema
from graphql import extend_schema, parse
extension = parse('extend type Query { health: String! }')
schema = extend_schema(schema, extension)`extend_schema` returns a new schema. Attach a resolver to each newly introduced executable field.
Handle subscription setup and events consume-subscription
from graphql import ExecutionResult, parse, subscribe
stream = await subscribe(schema, parse(source), context_value=context)
if isinstance(stream, ExecutionResult):
raise ValueError(stream.errors)
async for event in stream:
await socket.send_json(event.formatted)Setup errors return `ExecutionResult`; successful setup returns an async iterator. WebSocket protocol handling remains the server's job.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| strawberry-graphql | PyPI | Use it for annotation-driven schemas and maintained ASGI or framework adapters. |
| ariadne | PyPI | Use it when SDL is authoritative and resolvers should bind onto named schema fields. |
| graphene | PyPI | Use it for class-based schemas or an existing graphene-django codebase. |
More web backend guides
urllib3 · requests · ws · anyio · httpx · undici · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

