[ANN] PyFormattedStrings.jl: Python-like formatted interpolation

PyFormattedStrings performance is better: my cursory testing with @btime shows that it is 2-10 times faster than Strs.jl. Loading time is also shorter.

Do you have some benchmark code?
I’d like to see where I could improve the performance of Strs.jl.
That difference might no longer be there, I rewrote Format.jl to use the same code as Printf.jl, but incorporated so that I could handle extensions (and also because of the change from Grisu to Ryu in Base Julia)
I’m also interested in seeing the loading time for just StrFormat.jl compared to PyFormattedStrings.jl, which would be a more apples to apples comparison.

Not completely sure what you mean by keyword arguments here, but I do see (from the docs) that Strs.jl is designed to be extensible.

In StrFormat.jl, if you have f"\%(...)", that ends up calling fmt(...), and any argments, positional and keywords, are passed on to that. Same thing with f"\%<c format string>(...)", it calls cfmt(<c format string>, ...), and f"\{<python format string>}(...)" calls pyfmt(<python format string>, ...).

That’s just one way that StrLiterals.jl is designed to be extensible. It sets up the framework, so that other packages such as StrFormat and StrEntities can add new characters to be handled after the \ which otherwise would be errors. I’ve been planning on adding color sequences as well, I just need to decide on the syntax and then have a new StrColors.jl that would add that capability.

Normal Julia strings already can have executable code within the string, via interpolation, such as "Length of a is $(length(a))", so I don’t think that really holds.
The syntax I chose allows for new features to be added, simply by using other packages that use the StrLiterals framework, without breaking any other string syntax (so you can code generate these, just as you’d do for a normal Julia literal string, without worrying about having to now specially quote {, for example.

It may be too generous to call this “benchmark code” (:, but:

Benchmark
# PyFormattedStrings:
julia> @benchmark f"{123:d}"
BenchmarkTools.Trial: 
  memory estimate:  96 bytes
  allocs estimate:  2
  --------------
  minimum time:     48.535 ns (0.00% GC)
  median time:      50.056 ns (0.00% GC)
  mean time:        55.769 ns (4.82% GC)
  maximum time:     1.711 μs (94.67% GC)
  --------------
  samples:          10000
  evals/sample:     981

# Strs.jl:
julia> @benchmark f"\{d}(123)"
BenchmarkTools.Trial: 
  memory estimate:  808 bytes
  allocs estimate:  12
  --------------
  minimum time:     423.206 ns (0.00% GC)
  median time:      434.342 ns (0.00% GC)
  mean time:        467.090 ns (4.00% GC)
  maximum time:     7.331 μs (91.12% GC)
  --------------
  samples:          10000
  evals/sample:     199

Longer example also show a similar difference, sometimes more sometimes less.

Doesn’t look like StrFormat.jl is enough to use this interpolation, I get @f_str not defined. But I agree that loading just the required packages instead of all Strs.jl will likely give close loading times.

Sounds conceptually intersting. Do you have any neat usecases, examples that utilize this power? Outside of supporting different interpolation syntaxes.

As you could tell, PyFormattedStrings’ follows a very different design direction: just provide a familiar formatted interpolation syntax, nothing more nothing less. This makes the package very lightweight, and I don’t foresee it needing any significant maintenance while julia is at 1.x. There aren’t any major features missing, so I personally consider PyFormattedStrings stable/effectively frozen at this point.

Sure, and this is something I don’t really like. It would be cleaner to just have opt-in interpolation, such as the f-prefix.

As for escaping in Strs.jl, as I said above I could really understand how to do it. E.g., what goes instead of question marks so that f"???" == "\\(1)" ?

Yes, I’ve got a one-line fix for that I need to merge in. I’d always used using StrLiterals, StrFormat, StrEntitites, and StrLiterals exports the macros, but it doesn’t hurt to have any packages that use StrLiterals to also export them as well.

I use it all the time to pass the width and/or precision for things like f"\%f(foo, wid, pre)" (in C, that would be sprintf(“%*.*f”, foo, wid, pre), as well as adding options that aren’t part of the C formatting syntax.

Yes, recently, there has been another package trying to do the same thing (Fmt.jl), but that didn’t really match the Python syntax as well as your package does, and I didn’t feel it had all of the capabilities that StrFormat.jl has either.
I do very much like yours - do you have a list of things that are needed to fully match Python syntax?

If I understand what you want here, same as for a normal string, "\\(1)" (backslashes needs to be quoted with a backslash).

As far as loading time, I got the following:
“”“julia
julia> @time using PyFormattedStrings
0.192073 seconds (324.86 k allocations: 21.305 MiB, 1.97% compilation time)
“””
almost twice as slow loading, and twice as much memory, as StrFormat. This is with nightly.
“”“julia
julia> @time using StrLiterals,StrFormat
0.104242 seconds (182.17 k allocations: 11.804 MiB, 3.54% compilation time)
“””

Hm, I see. Don’t think I ever used this dynamic formatting in any language, but can imagine that it is useful sometimes.

I mentioned all differences I’m aware of in the README: Alexander Plavin / PyFormattedStrings.jl · GitLab. Now I also see that dynamic formatting (e.g. f"{x:{width}f}") is not supported - just didn’t consider it before. Looks definitely possible to implement after adding dynamic width and precision to printf by johanmon · Pull Request #40105 · JuliaLang/julia · GitHub lands into julia (:

Do I do anything wrong then?

julia> using StrLiterals, StrFormat
julia> "\\(1)"
"\\(1)"
julia> f"\\(1)"
"\\1"

I don’t have nightly installed, 1.6 gives the following:

➜  ~ julia --banner=no
julia> @time using StrLiterals,StrFormat
  0.415032 seconds (568.42 k allocations: 33.503 MiB, 3.17% gc time)
➜  ~ julia --banner=no
julia> @time using PyFormattedStrings
  0.363727 seconds (468.27 k allocations: 28.793 MiB, 3.85% gc time)
➜  ~ julia --startup-file=no --banner=no
julia> @time using StrLiterals,StrFormat
  0.137621 seconds (260.65 k allocations: 16.228 MiB, 34.82% compilation time)
➜  ~ julia --startup-file=no --banner=no
julia> @time using PyFormattedStrings
  0.082413 seconds (160.33 k allocations: 11.526 MiB, 51.27% compilation time)

But at this below-second scale loading time doesn’t seem important anyways.

1 Like

Thanks! I see that = isn’t implemented, that’s one of the things that I like to use, for centering the output in the field.

What I really find useful is the f"\%(...) formatting, which is based on Tom Breloff’s PR from 5 years or so ago to Formatting.jl, which was never merged in - which was one of the major reasons I created Format.jl (also, Formatting.jl at that point was not being maintained, for a couple years IIRC).
That allows you do set up defaults based on the type of the argument - as well as override the defaults with keyword arguments, so you don’t need to specify “e” for floating point, “d” for integer, “s” for string, it will pick those up based on the type, and you can easily control all of the parameters for how things are formatted.
I do think that’s the most convenient formatted output capability that I’ve seen in any language that I’ve worked with.

That’s interesting. I reproduced that with 1.6.1 as well on my laptop, but for 1.7, I still get better with StrFormat:

15:48 $ juliad --banner=no --startup-file=no
julia> @time using PyFormattedStrings
  0.179531 seconds (324.87 k allocations: 21.437 MiB, 2.11% compilation time)

julia>
(base) ✔ ~/j/julia [master ↓·1|…8]
15:49 $ juliad --banner=no --startup-file=no
julia> @time using StrLiterals,StrFormat
  0.106480 seconds (182.17 k allocations: 11.804 MiB, 3.57% compilation time)

While I totally agree, that for these subsecond times, it doesn’t matter that much, but I am intrigued by what might have slowed down using PyFormattedStrings by 3.6x between Julia 1.6.1 and today’d build of 1.7.

No, I think I must have! Thanks for pointing that out to me! Definitely a bug somewhere in my parsing code, I’ll have to fix that!

Unfortunately, Printf doesn’t support centering, so this is not possible to easily add.

Which is why I wouldn’t want to depend on Printf - I can fix any issues in Format.jl, and also not have it depend on a particular version of Julia (important for having all of the capabilities working for people who are sticking to the LTS version).

Indeed, there are both pros and cons. PyFormattedStrings used Formatting.jl when I first published it, but later switched to Printf. Main advantages were better compatibility for features I care about, and performance.

Update PyFormattedStrings v0.1.11

Notable new features and improvements:

  • Formatting templates/deferred formatting, with the ff"" macro to create a formatting function:
julia> tmpl = ff"age {age: 3d}, wt {weight:05.1f}, curyear {yr:d}"

# values are taken from the properties of the passed struct:
julia> tmpl((age=56, weight=87.5, yr=2023))
"age  56, wt 087.5, curyear 2023"

# or from the scope of tmpl definition:
julia> yr = 2023
julia> tmpl((age=56, weight=87.5))
"age  56, wt 087.5, curyear 2023"
  • Support variable width and precision on Julia 1.10+, with the same syntax as Python does:
julia> x = 12
julia> w = 10
julia> p = 4

julia> f"{x:{w}.{p}d}"
"      0012"
  • Reduced TTFX, from 0.7s to 0.15s on my laptop
7 Likes

An even bigger advantage is that PyFormattedStrings does not suffer from this catastrophic bug: Returns dramatically wrong results · Issue #108 · JuliaIO/Formatting.jl · GitHub, which would make it my go-to package if I need this kind of formatting! :tada:

2 Likes

PyFormattedStrings uses stdlib Printf, it just prepares the formatting string in the printf format and passes it.
This approach makes it automatically benefit from reliability of Printf and any improvements/bugfixes there!

2 Likes

Looks like Formatting.jl is too dangerous to use until/if that bug is fixed.

2 Likes

Love this package. I use python much more often than Julia currently so this makes switching to Julia a bit more seamless.

1 Like