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 ofchoices=becomeswtforms.SelectChoice(...).wtforms-datetime-local-field-DateTimeField(which renders the obsolete<input type="datetime">) becomesDateTimeLocalField.
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 intoSelectChoiceobjects, so tuple-unpacking breaks. See Templates that iterateform.X.choices. - Enum-backed selects. Nothing here needs migrating, but if you want the 3.3
enum_choices/enum_coercehelpers you convert by hand. See Enum-backed choices. - Edge cases the rules skip by design:
- Already-converted
SelectChoice(...)calls andDateTimeLocalFieldcall 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(...)afterfrom wtforms import DateTimeField fields.DateTimeField(...)without thewtforms.prefix (avoids clobbering unrelatedfieldsmodules)
- Already-converted
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:
- Run your formatter (
black,ruff format). ast-grep sometimes collapses multi-line tuple literals onto a single line. - Run your test suite. Pay extra attention to tests asserting on
rendered HTML, since
<input type="datetime">becomes<input type="datetime-local">. - 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
- Mike Fiedler (@miketheman)
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)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| wtforms_3_3_migrator-0.1.0.tar.gz | 6.9 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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 logSecurity 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.
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 logA 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.