Skip to content

[Python] - Improve Model Deserialization Perf - #11705

Draft
Kashif Khan (kashifkhan) wants to merge 6 commits into
mainfrom
kashifkhan/deserialization_perf
Draft

Kashif Khan (kashifkhan) wants to merge 6 commits into
mainfrom
kashifkhan/deserialization_perf

Conversation

@kashifkhan

@kashifkhan Kashif Khan (kashifkhan) commented Aug 17, 2026 •

Copy link
Copy Markdown
Member

This PR focusses on improving the performance of the deserialization path of python generated files. I wanted to focus on a couple areas where speed ups could be done and see some results

  • Do less work :) - today, when a model is built from a parsed payload, every value was sent to _serialize. Things like int, float, list[str] don't need to be serialized. Store them as is and work on things that need it
  • Look things up once, not every time — we compute two things one time per model class: a rest_name2field map, and the small list of fields that have client defaults. That way building each object skips re-scanning all the fields
  • Annotation Cache - figuring out how to deserialize a field (e.g. List[Pet], Optional[datetime]) means walking typing internals, and the answer never changes for a given type, so it's cached.
    On a ~5.6 MB DocumentIntelligence-shaped (why I started this) response on Python 3.10:

• Building the model tree: ~2.7× faster (~880 ms dropped to ~330 ms)
• Build + read every field: roughly halved (~2.0 s dropped to ~1.15 s)

@microsoft-github-policy-service microsoft-github-policy-service Bot added the emitter:client:python Issue for the Python client emitter: @typespec/http-client-python label Aug 17, 2026
@pkg-pr-new

pkg-pr-new Bot commented Aug 17, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@typespec/http-client-python@11705

commit: 3a1483c

@github-actions

github-actions Bot commented Aug 17, 2026 •

Copy link
Copy Markdown
Contributor

❌ There is undocummented changes. Run chronus add to add a changeset or click here.

The following packages have changes but are not documented.

  • ❌@typespec/http-client-python
Show changes

@github-actions

github-actions Bot commented Aug 17, 2026 •

Copy link
Copy Markdown
Contributor

Python emitter diff

Baseline gh:8a464a6a11ad21ed133c7155d99ad95eb335fbad vs this PR.

Diff summary: 206 file(s), +32759 / -3295

Rendered diff: inline on the run summary, or the emitter-diff-html artifact.

Informational check (eng/emitter-diff); does not block the PR.

@azure-sdk-automation

azure-sdk-automation Bot commented Aug 17, 2026 •

Copy link
Copy Markdown

You can try these changes here

🛝 Playground 🌐 Website 🛝 VSCode Extension

@kashifkhan
Kashif Khan (kashifkhan) force-pushed the kashifkhan/deserialization_perf branch from d85b447 to 5227b40 Compare August 18, 2026 21:33
@microsoft-github-policy-service microsoft-github-policy-service Bot added the stale Mark a PR that hasn't been recently updated and will be closed. label Sep 3, 2026
@microsoft-github-policy-service

Copy link
Copy Markdown
Contributor

Hi @Kashif Khan (@kashifkhan). Your PR has had no update for 14 days and it is marked as a stale PR. If it is not updated within 14 additional days, the PR will automatically be closed. If you want to refresh the PR, please remove the stale label.

@iscai-msft iscai-msft left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

had copilot take a look and it found these regressions, can you look into them? thanks!

self.value = value


def _construct_from_wire(cls: type, data: typing.Any) -> typing.Any:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Python values stop being converted into JSON-friendly values

Imagine Parent contains a Child, and Child has a datetime field:

parent = Parent(child={
    "timestamp": datetime(2026, 1, 1, tzinfo=timezone.utc)
})

Previously, creating the child converted that datetime into its wire representation:

parent.as_dict()
# Before: {"child": {"timestamp": "2026-01-01T00:00:00Z"}}
# After:  {"child": {"timestamp": datetime(...)}}

Why: The PR routes nested model construction through the new “already serialized” path. That assumption is correct for a server’s JSON response, but not for this caller-provided dictionary.

Impact: json.dumps(parent.as_dict()) now raises an error because JSON cannot directly encode a datetime. The datetime attribute should still be a datetime; the problem is its representation in as_dict().

Requested fix: Distinguish response data from caller input, and keep serializing caller-provided Python values.

return value


def _create_public_value(rf: typing.Optional["_RestField"], value: typing.Any) -> typing.Any:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing your input dictionary unexpectedly changes the model

Consider a nested child with a list:

data = {"labels": ["a"]}
parent = Parent(child=data)

# Later, change the original input:
data["labels"].append("b")

The resulting model differs:

parent.as_dict()
# Before: {"child": {"labels": ["a"]}}
# After:  {"child": {"labels": ["a", "b"]}}

Why: Previously, serialization created a separate list for the model. The new path stores the caller’s list directly, so both reference the same object.

The PR adds copying for positional construction:

Parent({"child": data})  # Protected by the new cloning helper

But not for these routes:

Parent(child=data)      # Keyword construction
parent.child = data     # Property assignment

Impact: Modifying input data can silently change a model you already constructed.

Requested fix: Preserve the previous separation from caller-owned containers on all public input paths. This is distinct from issue 1: copying a datetime does not serialize it, and copying containers solves a different problem.

return type(obj)(map(builtin, obj))
except (TypeError, ValueError):

def _lenient(entry: typing.Any) -> typing.Any:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

XML numeric lists return XML objects instead of numbers

Suppose an XML model declares an unwrapped List[int] field:

<Numbers>
    <value>1</value>
    <value>2</value>
</Numbers>

The XML parser supplies a list of XML elements, not a list of strings or numbers.

Previously, the element deserializer extracted each element’s text and converted it:

int(element.text)  # int("1") → 1

The new numeric shortcut instead does:

int(element)  # Fails: an XML element is not a number

Its fallback catches that failure and returns the original element.

model.values
# Before: [1, 2]
# After:  [<Element 'value' ...>, <Element 'value' ...>]

Requested fix: When the shortcut cannot convert an entry, use the existing element deserializer, which understands XML, rather than returning the entry unchanged.

@microsoft-github-policy-service

Copy link
Copy Markdown
Contributor

Hi @Kashif Khan (@kashifkhan). The PR will be closed since the PR has no update for 28 days. If this is still relevant please reopen.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

emitter:client:python Issue for the Python client emitter: @typespec/http-client-python stale Mark a PR that hasn't been recently updated and will be closed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants