The python generator — its skill
This file is the DESIGN of your ejected python generator (generators/python/):
to change the generator, edit this skill first, then make the code match it — a diff
to generators/python/ that has no covering sentence here is incomplete.
What it emits
One self-contained <stem>.py: typed dataclass models, a sync Client and an async
AsyncClient, and the embedded runtime. Python ≥ 3.9; the only dependency is
httpx (pip install httpx).
Design decisions that must hold
The file name is an importable module name. The
--outputstem follows the TypeScript convention (openapi.client.ts), andopenapi.client.pycannot be imported by name — nor can hyphens or a leading digit. The stem is converted withidentifierFor(stem, snake), sorebilly-core.client.tsemitsrebilly_core_client.pyandimport rebilly_core_clientjust works.Models are dataclasses by default, required fields first (a dataclass constraint), optionals
Optional[T] = None. Wire names live in a_field_map: ClassVar[Dict[str, str]]; decode/encode is reflective (_decode.py,get_type_hints) — no per-model codecs.models: pydanticemitsBaseModelclasses instead, for the FastAPI-shaped half of the ecosystem that expects them. A wire name becomesField(alias=…)withpopulate_by_name=True, so_field_mapis not emitted in this mode — the alias is the mapping. Everything else is unchanged: the same class names, the same field names, the sameOptional[T] = None, the same enums and union aliases, the same client and runtime. Switching modes must not change a call site.A discriminated union carries its discriminator into the pydantic annotation. The decoder hands a whole object tree to
model_validate, so a union nested in a model is resolved by pydantic and never reaches theDISCRIMINATORStable that dataclass mode walks. Pydantic resolves it correctly fromAnnotated[Union[...], Field(discriminator=…)], which it accepts only when every member types that property as aLiteral— and the mapping already pins one value per member, so the members getLiteral["cat"]. Such a union registers no table entry: pydantic owns it at every depth, and theLiteralmakes the decoder's member probe exact. A union whose members never declare the property keeps the plainUnionand the table entry, and pydantic then matches nested members its own way — the description is what has to change there.One runtime serves both model modes.
_decode.pydispatches on the target: a class withmodel_validateis validated by pydantic, a dataclass is hydrated reflectively, andencodemirrors that withmodel_dump(by_alias=True, exclude_none=True, mode="json"). A second runtime variant per mode would double the surface that has to stay in step, and pydantic'sValidationErroralready subclassesValueError, so union member probing needs no new except clause.models: pydanticadds a dependency, and the header says so. The default mode keeps httpx as the only requirement; the pydantic header asks for both. A mode that quietly needed a package the file never named would fail at import with nothing to act on.Every parameter is its own argument, so their names share one namespace with the arguments the method declares itself (
body,headers,timeout,retry,idempotency_key). Build them withuniqueIdentifiers(..., { taken: … }): OpenAPI lets one operation use a name in two locations (idin the path AND in the query), and adefthat declared one name twice is aSyntaxError. The wire name is untouched, so the request is unchanged.Naming: fields/methods snake*case via
identifierFor(..., RESERVED_WORDS.python); reserved words get a trailing underscore (class*);+1/-1becomeplus_1/minus_1.Enums are
class X(str, Enum)with SCREAMING members; unions areUnion[...]aliases. A DISCRIMINATED union registers its dispatch table in the runtime'sDISCRIMINATORSregistry (DISCRIMINATORS[Pet] = ("petType", {"cat": Cat, ...})), anddecode()routes through it —isinstancenarrowing works on decoded members. Undiscriminated unions decode by trying each member in order (the first that hydrates wins — see_decode.py). allOf is flattened viaflattenAllOf.Auth keys match the other languages.
auth={"apiKey": {...}}is the documented key — the same spelling TypeScript and PHP use, and the same as the scheme kind — withapi_keyaccepted as an alias so a snake_case config keeps working.Errors:
errorModemaps to raisingApiError(default) or returning aResultdataclass — the only generator with both modes outside TypeScript.Dates:
dateType: Dateannotatesformat: date-timeasdatetimeanddateasdate;_decode.pyparses ISO strings into them andencode()writesisoformat()back. The default (string) keeps the wire shape.Response headers: an operation that DECLARES success-response headers gains a
<op>_with_headers()variant (sync and async) returningEnvelope[T]—data,headers(coerced to int/bool/str with snake_case keys; absent/unparsable values omitted), and the rawresponse. Operations without declared headers get no variant, and the base method stays body-only.Servers: when the description declares servers, a
Serversclass is emitted with one static method per server; server VARIABLES become keyword arguments defaulting to the spec's defaults (Servers.production(organization_id="org_x")), so templated base URLs need no manual string building. The client's baked default staysservers[0]with variable defaults substituted.Parity surface: auth (bearer/basic/apiKey), retries with
Retry-After+ jittered backoff, timeouts, idempotency keys, middleware, pagination (<op>_pages()/<op>_items()+aitermirrors), SSE (iter_sse/aiter_sse), multipart.The runtime is hand-written in
runtime/*.pyin this folder and embedded as strings at prepare time — generator code never builds runtime logic from templates. Under--runtime modulethe same sources are written as sibling_*.pyfiles instead (package-relative imports become sibling imports; the client star-imports each module).Authored ONLY with the neutral toolkit (
Printer, naming, schema, pagination helpers) — the dogfooding guard fails otherwise.It documents itself. With
client.docs(or--docs), thedocshook writes<stem>.python.md: the security schemes, then one section per operation with its parameters, body, response type, and behavior notes. The call snippets come from this generator's ownsamplehook, so the page can only show the syntax of the SDK beside it, and the layout comes fromrenderReferencePagein the authoring toolkit — reachable from an ejected copy through@redocly/client-generator. Pagination on the page is decided bypaginationRuleFor, the same helper this generator resolves pagination with.
The modify loop
- Edit this skill: state the new behavior or decision.
- Make
generators/python/match it. - Run
redocly generate-clientand inspect thegit diffof the generated output — generated files are never hand-edited.
Newer built-in versions merge in with redocly eject-generator python --update.