Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@koxudaxi
Copy link
Contributor

@koxudaxi koxudaxi commented Jun 24, 2025

Implements PEP 750 template strings for MicroPython.

Started in discussion #17497. Template strings return Template objects instead of strings, allowing access to literal parts and expressions separately.

Changes:

  • Lexer/parser support for t-string syntax
  • Template and Interpolation types
  • Public type definitions in py/objtstring.h
  • Minimal builtin string.templatelib module
  • Conditional build with MICROPY_PY_TSTRINGS

Usage:

from string.templatelib import Template

t = t"Hello {name}!"
t.strings         # ('Hello ', '!')
t.values          # ('World',)
t.interpolations[0].expression  # 'name'

t"{x!r:>10}"      # Conversions: !r, !s, !a
rt"C:\{file}"     # Raw t-strings

Implementation:

  • Lexer (py/lexer.c) and parser (py/parse.c) handle tokenization
  • Template/Interpolation types in py/modtstring.c, py/objinterpolation.c
  • Public header py/objtstring.h eliminates code duplication
  • Minimal string module in py/modstring.c (no manifest.py changes)
  • Dynamic array doubling (4→8→16→...→2048), GC-safe
  • CPython 3.14 compatible error messages

Testing:

Tested on unix (coverage, standard, minimal), windows, webassembly.

New tests in tests/basics/:

  • string_module_tstring.py
  • tstring_basic.py, tstring_parser.py, tstring_errors.py
  • tstring_format.py, tstring_operations.py, tstring_coverage.py

Feature detection in tests/feature_check/tstring.py - automatic skip when MICROPY_PY_TSTRINGS=0.

All existing tests pass.

Trade-offs:

Code size: ~10 KB when enabled, zero when disabled.

String module: minimal builtin with only templatelib blocks micropython-lib's full string module. Future: coordinate with micropython-lib.

Config: enabled when MICROPY_CONFIG_ROM_LEVEL >= EXTRA_FEATURES, requires MICROPY_PY_FSTRINGS=1.

To disable: make CFLAGS_EXTRA=-DMICROPY_PY_TSTRINGS=0

@WebReflection
Copy link
Contributor

for what is worth it, we'd love to have this available at least for the PyScript WASM variant as this unlocks tons of UI related use cases we'd like to deliver to our users.

/cc @dpgeorge @ntoll

@koxudaxi koxudaxi changed the title py: Add PEP 750 template strings support. py: Add PEP 750 template strings support Jun 24, 2025
@codecov
Copy link

codecov bot commented Jun 25, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.43%. Comparing base (6773051) to head (d324425).
⚠️ Report is 22 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master   #17557      +/-   ##
==========================================
+ Coverage   98.38%   98.43%   +0.05%     
==========================================
  Files         171      174       +3     
  Lines       22297    23031     +734     
==========================================
+ Hits        21936    22670     +734     
  Misses        361      361              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions
Copy link

github-actions bot commented Jun 25, 2025

Code size report:

Reference:  esp32/boards: Add Silicognition ManT1S board definition. [27544a2]
Comparison: tests: Add .exp outputs for template string suite. [merge of d324425]
  mpy-cross: +7128 +1.880% [incl +128(data)]
   bare-arm:   +16 +0.028% 
minimal x86:  +126 +0.067% 
   unix x64: +14656 +1.708% standard[incl +896(data)]
      stm32:   +72 +0.018% PYBV10
     mimxrt:   +64 +0.017% TEENSY40
        rp2:   +88 +0.010% RPI_PICO_W
       samd:   +72 +0.026% ADAFRUIT_ITSYBITSY_M4_EXPRESS
  qemu rv32:   +60 +0.013% VIRT_RV32

@dpgeorge dpgeorge added the py-core Relates to py/ directory in source label Jun 25, 2025
Copy link
Member

@dpgeorge dpgeorge left a comment

Choose a reason for hiding this comment

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

Thanks for the contribution! This will be nice to have for the webassembly port.

I didn't do a review yet, but there will need to be tests to get full coverage of the new code.

@koxudaxi
Copy link
Contributor Author

@dpgeorge Thank you for the feedback and for taking time to look at this! I'm glad this will be useful for the webassembly port.

I'll add more tests to ensure full coverage when I find time.

@ntoll
Copy link
Contributor

ntoll commented Jun 25, 2025

@koxudaxi slightly off topic - but I notice you'll be at EuroPython in Prague, as will I. We should look out for each other and have a coffee or lunch together! 🇪🇺 🐍

@koxudaxi
Copy link
Contributor Author

@ntoll Sounds good! See you in Prague. ☕

@koxudaxi koxudaxi force-pushed the feature/pep750-template-strings branch 10 times, most recently from 7a1dc11 to 50dd4d1 Compare July 2, 2025 15:42
@koxudaxi koxudaxi requested a review from dpgeorge July 2, 2025 15:59
@koxudaxi
Copy link
Contributor Author

koxudaxi commented Jul 2, 2025

@dpgeorge

I didn't do a review yet, but there will need to be tests to get full coverage of the new code.

I tried really hard to make 100% coverage but some code is never called and I cannot cover it. Do you know how to fix this?

@koxudaxi
Copy link
Contributor Author

koxudaxi commented Jul 2, 2025

@dpgeorge
I'm not very familiar with the micropython codebase, so please let me know if you notice any issues with how I'm handling memory constraints. I think everything looks good, but I'd appreciate your review.

return MP_OBJ_FROM_PTR(result);
}

case MP_BINARY_OP_REVERSE_ADD: {
Copy link
Member

Choose a reason for hiding this comment

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

You can just remove this case, it's not needed.

@koxudaxi koxudaxi force-pushed the feature/pep750-template-strings branch 3 times, most recently from dcddb2a to e648074 Compare October 28, 2025 11:29
@AJMansfield
Copy link
Contributor

Feel free to ping me once you've finished working through this to a state that's ready to be re-reviewed.

@koxudaxi koxudaxi force-pushed the feature/pep750-template-strings branch from 3d82482 to 8d12ba3 Compare October 29, 2025 18:19
@WebReflection
Copy link
Contributor

@kumekay are you paying attention to hints? if you need any help at all, mind stating what is it?

I am following pushes and I feel like you are stuck in some testing loop and there's zero shame in that ... but maybe others can help with? Surely not helping your case but I see tons of pushes daily and I kinda sympathize with your possible frustration, specially when I see tests added or removed to make CI green .... maybe there's a better way, as everything else feels right in place so that asking "how do I make CI happy in there?" might be a faster way out.

@kumekay
Copy link
Contributor

kumekay commented Oct 30, 2025

@kumekay are you paying attention to hints? if you need any help at all, mind stating what is it?

I am following pushes and I feel like you are stuck in some testing loop and there's zero shame in that ... but maybe others can help with? Surely not helping your case but I see tons of pushes daily and I kinda sympathize with your possible frustration, specially when I see tests added or removed to make CI green .... maybe there's a better way, as everything else feels right in place so that asking "how do I make CI happy in there?" might be a faster way out.

@WebReflection
You probably need wanted to mention @koxudaxi here ;)

@WebReflection
Copy link
Contributor

@kumekay apologies and thank you for bringing my comment to @koxudaxi attention ... too many @k, my mistake 😅

@koxudaxi
Copy link
Contributor Author

koxudaxi commented Oct 30, 2025

@WebReflection
Thank you for your kind comment.
I'm indeed struggling a bit, but I'm fine-tuning the detailed behavior to match CPython and PEP 750 as closely as possible while also aligning with MicroPython's design principles.

I think I've squashed most of the bugs, and what remains is the module path issue.

@AJMansfield @dpgeorge
I'd like to discuss the module structure.
In t-strings, we need to provide the string.templatelib namespace from which classes like Template and Interpolation are imported.
from string.templatelib import Template
In CPython, this is provided as a Python file in the stdlib, but I'm wondering what the best way to arrange this in MicroPython would be. I was thinking of placing string.templatelib as a builtin, but I realized that if someone imports string from micropython-lib, it would overwrite these attributes. Is there a good approach for this?
I also considered moving templatelib entirely to micropython-lib, but then I realized this could complicate the build configuration.

https://docs.python.org/3/library/string.templatelib.html
https://github.com/python/cpython/blob/95a3564869daa3cc083e4d4603f5f03ee5f53e31/Lib/string/templatelib.py#L5

I believe everything else should be fine, so I'd appreciate another review when you have a chance.

@AJMansfield
Copy link
Contributor

AJMansfield commented Oct 30, 2025

I'd like to discuss the module structure. In t-strings, we need to provide the string.templatelib namespace from which classes like Template and Interpolation are imported. from string.templatelib import Template In CPython, this is provided as a Python file in the stdlib, but I'm wondering what the best way to arrange this in MicroPython would be. I was thinking of placing string.templatelib as a builtin, but I realized that if someone imports string from micropython-lib, it would overwrite these attributes. Is there a good approach for this? I also considered moving templatelib entirely to micropython-lib, but then I realized this could complicate the build configuration.

Normally, implementing a module as frozen mpy bytecode will actually be smaller than the equivalent implementation in C. It's also a common pattern to define an underscore-prefixed module in C to provide a minimal set of primitives to access some interpreter feature or api or perform the performance-critical hot loop of a package, and then implement the rest of that module in python. For an example of this, take a look at how the asyncio module is implemented. extmod/modasyncio.c doesn't actually define a python-api-compatible asyncio package, it just defines _asyncio to give C definitions for Task and TaskQueue, and then implements everything else in a python module --- extmod/asyncio/__init__.py and its siblings.

Instead of defining the recursive module structure manually in modstring.c and special-casing the import handling, this package structure could be implemented in python code, with a extmod/string/__init__.py, a extmod/string/templatelib.py, and then a modtstring.c that exports a bare _tstring module that exports the mp_obj_interpolation_t and mp_obj_template_t types.

Also, given the massive code size increase at stake here, it might even be worth considering if there's a way to move even more of the parse logic into something that can run from bytecode --- perhaps even, for some ports, as an externally mip-installable component rather than something to freeze in. For instance, have MicroPython's parser just minimally recognize t"(whatever)", and then invoke something along the lines of:

static mp_obj_t tstring_parse_func = NULL;
mp_obj_t tstring_parse(vstr_t *str) {
    if (tstring_parse == NULL) {
        mp_obj_t modtstring = mp_builtin___import__(1, &MP_OBJ_NEW_QSTR(MP_QSTR_mp_tstring_support));
        // NLR this to a "tstring support not installed" error? Maybe with directions to run `mip.install("mp_tstring_support")`?
        tstring_parse_func = mp_obj_dict_get(mp_obj_module_get_globals(modtstring), MP_QSTR_parse);
    }
    return mp_call_function_1(tstring_parse_func, mp_obj_new_str_from_vstr(str));
}

@koxudaxi
Copy link
Contributor Author

koxudaxi commented Nov 2, 2025

@AJMansfield
Thank you for the detailed comment. I applied your suggestions. Could you please review it?
Regarding the parser implementation, I had a misunderstanding. I thought writing it in C was better aligned with MicroPython's approach.
However, I tried to follow the f-strings implementation. Since we have enough tests, rewriting the parser in Python as you suggested is feasible.
About the mip-installable approach, my original plan was making t-strings a build option to avoid size increase for unnecessary targets. Could you clarify your suggestion?
Should it be mip-installable instead of a build option, or in addition to it?

(If I understand correctly, no changes to micropython-lib are needed.)

@AJMansfield
Copy link
Contributor

AJMansfield commented Nov 2, 2025

Thank you for the detailed comment. I applied your suggestions. Could you please review it?

Will do! Hopefully I'll be able to get to this on Monday.

I thought writing it in C was better aligned with MicroPython's approach.

That's my assumption as well --- I wouldn't put too much stock in my musings on that idea, for whether or not it might be possible to let portions of the parser operate from bytecode. If that's anything at all, it's certainly not something I'd put in the way of the current version being merged.

@koxudaxi koxudaxi force-pushed the feature/pep750-template-strings branch 2 times, most recently from f99c872 to 9d2d54a Compare November 10, 2025 16:47
@koxudaxi koxudaxi force-pushed the feature/pep750-template-strings branch from 9d2d54a to d4b5608 Compare November 10, 2025 16:54
@koxudaxi koxudaxi force-pushed the feature/pep750-template-strings branch from d4b5608 to d8c0a60 Compare November 10, 2025 17:24
@koxudaxi
Copy link
Contributor Author

@AJMansfield
I've reorganized the commits based on your previous feedback. If the commit granularity is still too coarse or inappropriate, please let me know and I'll revise them.

@AJMansfield
Copy link
Contributor

Apologies for the delay reviewing, finally found the time though. And that's a well-organized set of commits I'll have no trouble examining.

Copy link
Contributor

@AJMansfield AJMansfield left a comment

Choose a reason for hiding this comment

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

On examination of the code and after running it on my machine, I can say that the functional code in this PR seems substantively correct, but there's a few details to still clean up especially with regards to testing.

Most of the tests here do not pass when run in comparison with a real CPython 3.14 instance (i.e. --- but with only a small number of lines differing, on error behavior and other areas where differences are commonly accepted. Usually, when testing a feature that's broadly compatible but with a minor deviation, it's a common pattern to separate out the compatible and incompatible cases between e.g. some_test.py and some_test1.py, so that you can provide a some_test1.py.exp without removing the cross-compare for all of it.

It's reasonable enough to also be using .exp files here to bridge the gap to 3.14-less testing environments, but I'd like to still see these tests split into separate fully-3.14-compatible and non-3.14-compatible test files. Note that the .exp's for the non-3.14-compatible ones should go in the same commit with the tests --- i.e. it should be a complete commit with everything needed to run and pass a comparison in an environment that does have the right CPython, with the follow-up commit containing only exactly the files generated by running e.g. python3.14 some_test.py &> some_test.py.exp for the relevant tests.

(As a related note, we've actually been needing a mechanism to address this scenario for a while --- ever since CPython changed tracing formats making MicroPython's outputs on the tests for sys.settrace no longer match on newer versions. If you'd be interested in taking on adding that --- maybe a table similar to tests_with_regex_output that gives CPython version ranges to use or disregard exp files on, that would be and then use or disregard their exp file when appropriate --- it would be quite useful indeed to have. If not though, I'll be satisfied with just having the tests split.)

Copy link
Contributor

Choose a reason for hiding this comment

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

This exp should be in the commit that adds the feature check; it's needed to even run the tstring tests at all.

Copy link
Contributor

Choose a reason for hiding this comment

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

This should probably be a micropython test rather than a basics test, as __template__ is a micropython-specific implementation detail.

py/modbuiltins.c Outdated
#endif
{ MP_ROM_QSTR(MP_QSTR_ZeroDivisionError), MP_ROM_PTR(&mp_type_ZeroDivisionError) },
#if MICROPY_PY_TSTRINGS
{ MP_ROM_QSTR(MP_QSTR___template__), MP_ROM_PTR(&mp_builtin___template___obj) },
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the reasoning for putting this at the end, rather than with the other double-underscore builtins? (Note that __print_repl__ is actually also a micropython-specific builtin.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've moved __template__ to group it with the other double-underscore builtins.

@koxudaxi koxudaxi force-pushed the feature/pep750-template-strings branch from d8c0a60 to 90343b7 Compare November 16, 2025 03:55
@koxudaxi koxudaxi force-pushed the feature/pep750-template-strings branch from 90343b7 to d324425 Compare November 16, 2025 10:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

py-core Relates to py/ directory in source

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants