Skip to content

by CLI reference

basedpython ships with two executables: by and buff. by is the basedpython driver — an extension of ty and includes the type-checker, the transpiler, and a few project-level commands

buff is the basedpython version of ruff

by <command> [args...]

in addition to the cli provided by ty, by includes:

command what it does
run transpile and run a module with python -m <module>
build transpile every .by/.byi file and write to build/
compile compile .by files to native CPython extension modules
generate-api-file write a public-api lockfile (see api-lock)
transpile transpile a single file to stdout (reads stdin if no file)

by run

by run MODULE [ARGS...]             # transpile + run with `python -m MODULE`
by run                              # run the configured entry point
by run MODULE --min-version 3.12    # target a specific runtime python version

equivalent to by build && python -m MODULE, but only transpiles the modules required to import MODULE

the module can be left out when the project configures an entry point:

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

by run then runs app.cli. a module named on the command line always wins, so by run other still runs other. the first positional argument is always the module — to reach the entry point's own arguments, name it: by run app.cli --name asdf

everything after MODULE is forwarded to the program as sys.argv[1:], including options — by run main --name asdf passes --name asdf on. the one exception is a leading -h / --help, which prints by run's own help; write by run main -- --help to reach the program's. when the program's entry point is a main function, those arguments are parsed into its parameters

the project is type-checked first, and a program with check errors is not run — the checker's verdict and the runtime must not diverge. warnings don't block; a rule can be downgraded in configuration where its error is unwanted

the interpreter is the project's own: the environment by check resolves imports against — the environment.python the project configures, else an activated virtual environment, a conda environment, or a .venv beside the project's pyproject.toml. --python overrides it for one run, and PYTHON stands in only where the project has no environment of its own, above the bare python3 discovery falls back to. a third-party package lives in that environment's site-packages and nowhere else, so running anywhere else fails on an import the checker had no complaint about. all of that resolves against the project root rather than the working directory, so by run in a subdirectory is still this project

the emitted code targets that interpreter's version by default. an explicit --min-version wins, but must not exceed the interpreter — by run refuses rather than emit code the interpreter cannot parse. an interpreter older than the version the project declares is refused too: the source may use syntax that python has no lowering for, and the failure would land as a SyntaxError inside generated code. passing --min-version for the interpreter's own version builds for it instead

the program runs in the directory by run was invoked from, the way python -m does. the transpiled tree lives in a temporary directory, which python puts at the head of sys.path — so a relative path on the command line, and anything the program reads or writes beside the project, resolve where they were written to

hidden directories (.claude, .git, .venv, …) and build outputs are never treated as project source: they are neither checked nor transpiled, by run and build alike

by build

by build                            # transpile every .by/.byi the project claims
by build --min-version 3.12         # target a specific runtime python version

by build walks the project's own file set — the one by check walks — so src.exclude and the ignore files it honours apply here too, and the two halves of the toolchain never disagree about which files are in the project. hidden directories (.claude, .git, .venv, …) and build outputs are skipped

without --min-version the emit target is the project's configured python version (environment.python-version, else the requires-python lower bound), so the checker and the emitter agree about which python this project targets

a file that fails to parse fails the build, but only for itself: every other module is still written. a code generator and a test runner are exactly what you reach for when one file is mid-edit

artifacts and exit status answer different questions. everything that could be emitted is emitted, sourcemap and package markers included; the exit status says whether anything was reported. so a build that prints an error exits 1 while still leaving a usable build/, and by build && pytest build/tests runs the tests only against a tree the checker had nothing to say about

writes the transpiled python to ./build/ mirroring the module tree. a src-layout project's src/package_name/main.by is the module package_name.main, so it lands at build/package_name/main.pybuild/ is a directory you can put on sys.path as it stands, and run.main names a module the same way an import does. by generate-api-file does not read build/ as first-party source — it is regenerated on every build. by check still walks it, though, so a project that keeps its output beside its sources will see diagnostics reported against generated python; exclude it with src.exclude = ["build"] if that is in the way

alongside the python it writes build/_by_sourcemap.py, mapping each generated line back to the .by line it came from, with a digest of both files so a tool reading it can tell whether it still describes what is on disk — see sourcemaps

by compile

status: early. integer, float and bool arithmetic, control flow, and calls within a module compile natively. everything else falls back to the interpreted definition — see native compilation

by compile                      # every .by file under the project root → build/
by compile hot.by               # one file
by compile -o native hot.by     # a different output directory
by compile --verbose            # report every function left interpreted, and why
by compile --emit-c-only        # write the generated C without compiling it
by compile --no-any             # refuse to leave a gradual-typed function interpreted
by compile --require-native     # refuse to leave *any* function interpreted

the output directory mirrors the module tree, the way by build's does: the package member pkg/sub/dup.py lands at build/pkg/sub/dup.cpython-313-darwin.so, and the package pkg/sub/__init__.py at build/pkg/sub/__init__.cpython-313-darwin.so — so build/ can go on sys.path as it stands and every module imports under the dotted name it was compiled as

by compile writes everything by build writes, and the extensions as well. a compiled module is not a program on its own: it reads its templates and fixtures relative to itself, and imports the modules beside it. so the tree holds the whole project as importable python — every .by transpiled, every hand-written .py, py.typed, the data files, the sourcemap — with a native extension beside each module that was compiled. python's finder prefers an extension to source, so those modules load natively and the rest are interpreted, which is what makes naming files a speed decision rather than a correctness one:

by compile hot.by        # hot is native, every other module is interpreted

naming files decides what is compiled, not what is written: the whole project is checked and transpiled either way, so by compile hot.by costs what a by build costs plus the one module's native compile. that is the price of the tree being a program rather than a heap.

build/ is a mirror rather than a pile. what the previous run wrote and this one did not is taken back, which matters most for extensions: python prefers one to source, so an extension left behind by a deleted or renamed module would go on shadowing the .py written in its place. the artefacts therefore describe the last invocation — by compile a.by then by compile b.by leaves b native and a interpreted, and --emit-c-only, which stops before the C compiler, leaves a tree with no extensions at all. in every case each module still imports, so this costs speed rather than correctness; by compile with no arguments compiles the whole project.

the directory is shared with by build on purpose — the same tree, built two ways — and the two take each other's output back accordingly. it is also a name setuptools and python -m build use, so a project using both writes them into one directory; nothing is destroyed, because the manifest only ever takes back what by itself wrote.

a directory holding a .by-manifest is a build output, whoever wrote it, and is never read as source or carried into another tree — a project that builds into two directories would otherwise put a copy of each inside the other. that is by the marker rather than by the directory's name, because --out can say anything. a project that deliberately ships a directory containing one has to rename the marker or keep the directory outside its source roots.

upgrading from a version that wrote out/: nothing migrates it, and nothing reads it any more. delete it — a stale out/ holds importable python that no build refreshes.

--no-any buys no speed on its own — it is a predictability contract. a gradual type is the commonest reason a function silently stays interpreted, and a decline is invisible unless you look for it, so a module that means to be fully compiled can say so and be held to it:

$ by compile app.by --no-any
error: could not compile app.by

Caused by:
    `no-any` is on and 1 function(s) could not be compiled because a type was gradual:
      loose: a gradual type has no known representation

--require-native asks a different, stricter question. --no-any asks is this module fully typed; --require-native asks does this module compile entirely, and so also fails on a type that is perfectly precise but that the compiler does not represent yet:

$ by compile app.by --require-native
error: could not compile app.by

Caused by:
    `require-native` is on and 1 function(s) were left interpreted:
      describe: `list[int]` has no native representation yet

a function the compiler cannot lower natively is not an error: the module's transpiled python is embedded in the extension and executed at import, so declined functions still exist and module-level code still runs. the natively compiled functions are installed over the top

$ by compile hot.by --verbose
hot.by -> build/hot.cpython-313-darwin.so
  declined describe: `list[int]` has no native representation yet

compiled 1 module(s)
1 function(s) left to the interpreted definition

the C toolchain and the cpython headers come from the same interpreter by run uses, chosen the same way, and it must have development headers available. it has to be the same one: an extension built against one abi is unimportable by another, and by run --compiled imports what this wrote

by generate-api-file

by generate-api-file                       # writes ./api.lock
by generate-api-file --stdout              # writes lockfile to stdout
by generate-api-file -o public.lock        # custom output path
by generate-api-file --python-version 3.10 # target a specific python version

see api-lock for the lockfile format and workflow

by transpile

by transpile FILE           # read FILE, write transpiled python to stdout
by transpile                # read from stdin, write to stdout
by transpile FILE --reverse         # convert python source into basedpython idioms
by transpile FILE --min-version 3.12 # target a specific runtime python version
echo 'x: int = 1' | by transpile

by transpile also accepts a directory, transpiling it in place (every .by.py, or with --reverse every .py.by)

stops at the first transpile error and prints a diagnostic

a python source that declares its own encoding with a PEP 263 comment is decoded as it is read. what is written back out is utf-8, so the declaration is rewritten to say utf-8 — left alone it would name an encoding the file no longer has. utf-8 and the latin-1 family are decodable; a file declaring anything else is skipped and named rather than guessed at