Skip to content

getting started

install by, write a .by file, and run it. everything below takes about five minutes

installation

basedpython ships as the basedpython package, which installs two executables: by, the type checker and transpiler, and buff, the linter and formatter

uv add --dev basedpython

verify it works:

by --help

your first file

basedpython source files use the .by extension. create main.by:

message = "hello"
print(message)

run it directly:

by run main

by run main finds main.by in the current directory, transpiles it (and all other .by files in the project), then executes it with your python interpreter

by run takes a module, not a path

the argument is what you would pass to python -m, so it is main, not main.by — and a nested entry point is pkg.main

naming the module every time gets old once a project has an entry point. configure one and by run alone is enough:

[tool.basedpython.run]
main = "main"

that goes in the [tool.basedpython] section of pyproject.toml, or in a basedpython.toml beside it, which holds the same options at the top level:

[run]
main = "main"

see configuration for everything that can go in there

building

by build writes the project to build/ as python:

by build
main.by -> build/main.py
utils.by -> build/utils.py

build complete (2 files)

that is the whole project, not only its .by files — a hand-written .py module, a py.typed, a data file the program reads are all carried across to the same place, so build/ runs the way the source does

the generated .py files are ordinary python. run them with any python tool:

python build/main.py
pytest build/
mypy build/

to ship the project rather than run it, build a wheel — see packaging:

uv build

CI integration

- name: Build
  run: |
    uv add --dev basedpython
    by build

- name: Test
  run: pytest build/

converting python to basedpython

you don't have to start from an empty file. by transpile --reverse runs the transpiler backwards, rewriting python source into the basedpython idiom it would have lowered from:

by transpile --reverse legacy.py
from typing import Callable, Optional


class Node:
    children: list["Node"]

    def __eq__(self, other: object) -> bool:
        if other is self:
            return True
        return isinstance(other, Node) and other.children == self.children

    def find(self, key: str) -> Optional["Node"]: ...


on_visit: Callable[[Node], None]

comes back as:

from typing import Optional


class Node:
    children: list[Node]

    def __eq__(self, other: object) -> bool:
        if other === self:
            return True
        return other is Node and other.children == self.children

    def find(self, key: str) -> Optional[Node]


on_visit: (Node) -> None

the identity fast path became === and the isinstance became is, the quotes came off the self-references, Callable[[Node], None] became an arrow type, the : ... body became an empty declaration, and the now-unused Callable import was pruned

point it at a directory to convert a whole tree in place, every .py to a .by:

by transpile --reverse src/

each reverse transform mirrors a forward one, so a converted file transpiles back to the program you started with

read the diff

reversing converts the constructs that have a reverse transform and leaves the rest alone — __init__ does not become init(...), Optional[T] does not become T?. it is a head start, not a port. run by check on the result, and read differences from python before you commit it

low-level: single file transpilation

by transpile is the low-level command for single-file transforms. it reads a file (or stdin) and writes the transpiled python to stdout:

by transpile hello.by
echo 'a = b ?? 1' | by transpile
# → a = b if b is not None else 1

output goes to stdout - redirect it to a file if you want to keep it (by transpile hello.by > hello.py). use by build to transpile a whole project into build/

forward references

basedpython has no manual forward-reference syntax — a string in an annotation is a string-literal type, not a deferred name. so when a class refers to itself before its definition finishes, the transpiler quotes the reference for you:

class Node:
    def next(self) -> Node: ...   # → def next(self) -> "Node": ...

quoting only happens when it's needed. on python 3.14+ annotations are evaluated lazily (PEP 649), and if you target an older runtime but want every annotation deferred anyway you can opt into a blanket from __future__ import annotations — in either case the reference is left as-is

next