Skip to main content

wtforms-3.3-migrator

ast-grep rules that rewrite Python source to clear deprecations introduced in WTForms 3.3.x

The pack is deliberately narrow: two syntactic rewrite rules, plus a handful of things it leaves for you to do by hand. Skim "What it does and doesn't do" below before you run it, so the gaps don't surprise you.

What it does and doesn't do

Rewrites automatically:

  • wtforms-select-choice - the legacy (value, label) tuple form of choices= becomes wtforms.SelectChoice(...).
  • wtforms-datetime-local-field - DateTimeField (which renders the obsolete <input type="datetime">) becomes DateTimeLocalField.

Leaves for you to do by hand:

  • Jinja templates (and any code) that unpack form.X.choices. The rewrite turns choice entries from 2-tuples into SelectChoice objects, so tuple-unpacking breaks. See Templates that iterate form.X.choices.
  • Enum-backed selects. Nothing here needs migrating, but if you want the 3.3 enum_choices / enum_coerce helpers you convert by hand. See Enum-backed choices.
  • Edge cases the rules skip by design:
    • Already-converted SelectChoice(...) calls and DateTimeLocalField call sites
    • Tuples passed via a variable (choices=my_choices) - the literal isn't visible
    • Tuples in unrelated keyword arguments (headers=[("X-Foo", "bar")])
    • 3-tuples in choices= (e.g. (value, label, render_kw)) - adjust if needed
    • Bare DateTimeField(...) after from wtforms import DateTimeField
    • fields.DateTimeField(...) without the wtforms. prefix (avoids clobbering unrelated fields modules)

Rules

wtforms-select-choice

Rewrites the legacy (value, label) tuple form of choices= to the new wtforms.SelectChoice helper.

-wtforms.SelectField(choices=[("a", "A"), ("b", "B")])
+wtforms.SelectField(choices=[wtforms.SelectChoice("a", "A"), wtforms.SelectChoice("b", "B")])

Handles list literals, list comprehensions, assignments to .choices (field.choices = [...] / field.choices += [...]), and comparison assertions in tests (assert form.field.choices == [...]).

wtforms-datetime-local-field

Renames DateTimeField (renders the obsolete <input type="datetime">) toDateTimeLocalField.

-wtforms.fields.DateTimeField(render_kw={"readonly": True})
+wtforms.fields.DateTimeLocalField(render_kw={"readonly": True})

Matches wtforms.DateTimeField and wtforms.fields.DateTimeField. A bare DateTimeField(...) from from wtforms import DateTimeField is intentionally not rewritten - that case is rare and the import line needs its own edit anyway, so do those by hand.

Install

pip install wtforms-3.3-migrator

That pulls in ast-grep (via the ast-grep-cli wheel) and gives you the wtforms-3.3-migrator command. Requires Python 3.10+ to run; it migrates source for any Python 3 target.

Usage

Run it from your project root. It previews by default and rewrites in place with --update-all:

# Preview the changes
wtforms-3.3-migrator .

# Apply the rewrites in place
wtforms-3.3-migrator --update-all .

# Scan just part of the tree (faster, smaller diff)
wtforms-3.3-migrator --update-all src/ tests/

# Run a single rule
wtforms-3.3-migrator --rule wtforms-datetime-local-field --update-all .

The command always scopes the scan to this pack's rules, so it won't touch unrelated # ast-grep-ignore directives elsewhere in your code.

From a git checkout (without installing)

To hack on the rules or pin to a specific revision, skip the package and point ast-grep at the pack's sgconfig.yml yourself. You'll need ast-grep 0.30+ on its own (tested on 0.42):

brew install ast-grep        # macOS
cargo install ast-grep       # any platform
pipx install ast-grep-cli    # via pip

git clone https://github.com/miketheman/wtforms-3.3-migrator.git ~/code/wtforms-3.3-migrator
cd ~/code/my-project

# Preview, then apply
ast-grep --config ~/code/wtforms-3.3-migrator/sgconfig.yml scan \
  --filter '^wtforms-' .
ast-grep --config ~/code/wtforms-3.3-migrator/sgconfig.yml scan \
  --filter '^wtforms-' --update-all src/ tests/

# A single rule
ast-grep --config ~/code/wtforms-3.3-migrator/sgconfig.yml scan \
  --filter '^wtforms-datetime-local-field$' --update-all .

A shell alias

Running this against more than one repo? An alias saves typing:

alias wtforms-migrate='ast-grep --config ~/code/wtforms-3.3-migrator/sgconfig.yml scan --filter "^wtforms-"'

# then, from any project:
wtforms-migrate .                  # preview
wtforms-migrate --update-all .     # apply

Reviewing the diff

These are syntactic rewrites. Before applying, commit or stash anything in flight - you'll want a clean slate to diff against. Then:

  1. Run your formatter (black, ruff format). ast-grep sometimes collapses multi-line tuple literals onto a single line.
  2. Run your test suite. Pay extra attention to tests asserting on rendered HTML, since <input type="datetime"> becomes <input type="datetime-local">.
  3. Spot-check choices= declarations that pull from a variable. The rule can't see inside a variable, so those aren't touched.

git checkout -- <file> reverts a single file; git reset --hard throws the whole rewrite away.

Templates that iterate form.X.choices

The wtforms-select-choice rewrite changes each entry of a choices list from a 2-tuple (value, label) into a 4-field SelectChoice NamedTuple (value, label, selected, render_kw). Anywhere downstream code tuple-unpacks a choice into two names, it will break:

{# Before: works on tuples #}
{% for value, label in form.role_name.choices %}
  {{ value }} - {{ label }}
{% endfor %}

{# After: use attribute access #}
{% for choice in form.role_name.choices %}
  {{ choice.value }} - {{ choice.label }}
{% endfor %}

ast-grep does not (yet) scan Jinja templates - check for this pattern yourself with a quick grep:

grep -rn 'for.*,.*in.*\.choices' templates/

The same applies to any Python code that unpacks form.X.choices (rare, but worth checking).

Enum-backed choices

There is no rule for Enum-backed selects, on purpose, but here is the lay of the land if you have them.

Nothing here needs migrating from 3.2: a bare coerce=SomeEnumClass does a plain value lookup (Enum(v)) in 3.3, the same as 3.2, so it keeps working unchanged.

3.3 does add helpers for declaring enum-backed selects, and they read better than hand-rolling the choice list. For new code, prefer them:

from wtforms.fields import enum_choices, enum_coerce

color = SelectField(
    choices=enum_choices(Color),
    coerce=enum_coerce(Color),
)

enum_choices(E) builds the option list; by default the HTML value is member.value and the label is str(member) (so member.value for a StrEnum, otherwise member.name). enum_coerce(E) resolves the submitted string back to a member - by default the same value lookup a bare coerce=E already does, so the two are interchangeable for value-keyed forms. Pass by="name" to both helpers to key on member.name instead, and label=<callable> to customize the displayed text.

Converting an existing hand-rolled list is a manual call, not a mechanical rewrite, because the right replacement depends on what the old code put in the label and on the wire:

# Safe: label is the value. enum_choices(E) reproduces it for a StrEnum.
choices=[wtforms.SelectChoice(role.value, role.value) for role in Role]
# -> choices=enum_choices(Role)

# NOT a drop-in: label is member.name, not the value. Bare enum_choices(E)
# would relabel the options to their values. Keep the labels explicitly:
choices=[wtforms.SelectChoice(i.value, i.name) for i in IssuerType]
# -> choices=enum_choices(IssuerType, label=lambda m: m.name)

A syntactic tool can't tell those two apart (it depends on each Enum's value vs name and its __str__), which is why this is documented here rather than auto-fixed.

Verifying the pack itself

If you've edited the rules locally, or just upgraded ast-grep and want to make sure nothing broke, cd into the pack and run its tests:

cd ~/code/wtforms-3.3-migrator
ast-grep test

All rules should pass. Regular users don't need to bother with this.

Authors

License

Public domain / CC0. Copy it, fork it, do what you want. Don't blame me if it eats your code.

Metadata

Release files for wtforms-3.3-migrator 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for wtforms-3.3-migrator 0.1.0
File Size Uploaded Provenance
wtforms_3_3_migrator-0.1.0.tar.gz 6.9 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for wtforms-3.3-migrator 0.1.0
File Interpreter ABI Platform
wtforms_3_3_migrator-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 16.8 kB

Release files / wtforms_3_3_migrator-0.1.0.tar.gz

Download URL wtforms_3_3_migrator-0.1.0.tar.gz
Size 6.9 kB
Tags Source
SHA-256 checksum
How to use checksums
b8aa830ee2e6d93060abef25361d8a863f6bc08cacc45c8f65661e500cdfede2
BLAKE2b-256 checksum
How to use checksums
d444e0eb78e8bd8644caa0a92833dae448d96b89f8e3786ab097338fb1c31aa6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.13

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jun 16, 2026.

Transparency log

Release files / wtforms_3_3_migrator-0.1.0-py3-none-any.whl

Download URL wtforms_3_3_migrator-0.1.0-py3-none-any.whl
Size 9.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
cdd12cdcd60c88784e7c251ea0fb002ef993c91cec27b5f5e9029092e0cba48e
BLAKE2b-256 checksum
How to use checksums
9facba966fab5e2e5402d826ade67bdfa4c0138b94bd554d366fdaa490c48d34
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.13

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jun 16, 2026.

Transparency log

Security for wtforms-3.3-migrator 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for wtforms_3_3_migrator-0.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jun 16, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page