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) to a temporary directory, then executes python -m main from there

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 transpiles all .by files in the project and writes the output to out/, mirroring the source structure:

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

build complete (2 files)

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

python out/main.py
pytest out/
mypy out/
ruff check out/

CI integration

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

- name: Test
  run: pytest out/

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 out/

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