Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
bfb093a
Remove debug_process.py file
doubledare704 Apr 3, 2025
87bcd4b
🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
pre-commit-ci[bot] Apr 3, 2025
73bd728
Fix linting issues and add type annotations
doubledare704 Apr 3, 2025
2766fe7
Fix mypy type error in BytesParamType.convert
doubledare704 Apr 3, 2025
38145f5
🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
pre-commit-ci[bot] Apr 3, 2025
c0ce2ce
make full coverage of tests
doubledare704 Apr 3, 2025
8a52f94
🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
pre-commit-ci[bot] Apr 3, 2025
de03a32
avoid assert false
doubledare704 Apr 3, 2025
dc98576
avoid coverage in 1 line of tests
doubledare704 Apr 3, 2025
3f94475
Merge branch 'master' into feature/support-bytes-type
doubledare704 Sep 6, 2025
32520a7
Merge branch 'master' into feature/support-bytes-type
doubledare704 Sep 12, 2025
8eb5fcc
Merge branch 'fastapi:master' into feature/support-bytes-type
doubledare704 Sep 27, 2025
963b0d1
feat(bytes): support encoding/errors for bytes params\n\n- Add encodi…
doubledare704 Sep 27, 2025
b5111fc
fix(tests): correct stderr assertion in bytes encoding test
doubledare704 Sep 27, 2025
a59129f
fix(tests): correct stderr assertion in bytes encoding test
doubledare704 Sep 27, 2025
2e8b8ec
fix(tests): correct stderr assertion in bytes encoding test
doubledare704 Sep 27, 2025
996a5d7
fix(tests): correct stderr assertion in bytes encoding test
doubledare704 Sep 27, 2025
77c2846
set no cover for uncoverable line
doubledare704 Sep 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions docs/tutorial/parameter-types/bytes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Bytes

You can declare `bytes` for CLI arguments and options.

By default, `bytes` are created by encoding the input string with UTF-8 (the same as Python's default for `str.encode()`), but you can configure the encoding and error handling.

## Default UTF-8 encoding

This example declares a `bytes` argument using the default UTF-8 encoding:

{* docs_src/parameter_types/bytes/tutorial001.py *}

Try it with non-ASCII characters and you will get UTF-8 encoded bytes.

## Custom encoding on Argument

You can set a specific encoding for a `bytes` argument:

{* docs_src/parameter_types/bytes/tutorial002.py hl[4] *}

Here the argument is configured with `encoding="latin-1"`, so the command line input will be encoded accordingly.

## Custom encoding and errors on Option

You can also configure a `bytes` option with a specific encoding and error handling mode:

{* docs_src/parameter_types/bytes/tutorial003.py hl[4] *}

The `errors` parameter supports the same values as Python's `str.encode()` (e.g. `"strict"`, `"ignore"`, `"replace"`).

## Primary use case

The goal of supporting `bytes` is to let you write a single function that works both:

- Inside Typer: when called as a CLI, Typer parses command line input and converts it to `bytes` using the configured `encoding`/`errors`.
- Outside Typer: when called as regular Python code, you can pass `bytes` directly, without any CLI parsing involved.

This keeps your function reusable in both contexts while giving you control over how CLI text inputs are converted to `bytes`.
Empty file.
10 changes: 10 additions & 0 deletions docs_src/parameter_types/bytes/tutorial001.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import typer


def main(data: bytes):
# Default encoding is UTF-8
print(f"Bytes: {data!r}")


if __name__ == "__main__":
typer.run(main)
10 changes: 10 additions & 0 deletions docs_src/parameter_types/bytes/tutorial002.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import typer


def main(data: bytes = typer.Argument(..., encoding="latin-1")):
# Argument configured to use latin-1
print(f"Bytes (latin-1): {data!r}")


if __name__ == "__main__":
typer.run(main)
10 changes: 10 additions & 0 deletions docs_src/parameter_types/bytes/tutorial003.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import typer


def main(token: bytes = typer.Option(..., encoding="ascii", errors="replace")):
# Option configured with ascii encoding and errors=replace
print(f"Token: {token!r}")


if __name__ == "__main__":
typer.run(main)
155 changes: 155 additions & 0 deletions examples/bytes_encoding_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import base64
import binascii

import typer

app = typer.Typer()


@app.command()
def base64_encode(text: bytes):
"""Encode text to base64."""
encoded = base64.b64encode(text)
typer.echo(f"Original: {text!r}")
typer.echo(f"Base64 encoded: {encoded.decode()}")


@app.command()
def base64_decode(encoded: str):
"""Decode base64 to bytes."""
try:
decoded = base64.b64decode(encoded)
typer.echo(f"Base64 encoded: {encoded}")
typer.echo(f"Decoded: {decoded!r}")
typer.echo(f"As string: {decoded.decode(errors='replace')}")
except Exception as e:
typer.echo(f"Error decoding base64: {e}", err=True)
raise typer.Exit(code=1) from e


@app.command()
def hex_encode(data: bytes):
"""Convert bytes to hex string."""
hex_str = binascii.hexlify(data).decode()
typer.echo(f"Original: {data!r}")
typer.echo(f"Hex encoded: {hex_str}")


@app.command()
def hex_decode(hex_str: str):
"""Convert hex string to bytes."""
try:
data = binascii.unhexlify(hex_str)
typer.echo(f"Hex encoded: {hex_str}")
typer.echo(f"Decoded: {data!r}")
typer.echo(f"As string: {data.decode(errors='replace')}")
except Exception as e:
typer.echo(f"Error decoding hex: {e}", err=True)
raise typer.Exit(code=1) from e


@app.command()
def convert(
data: bytes = typer.Argument(..., help="Data to convert"),
from_format: str = typer.Option(
"raw", "--from", "-f", help="Source format: raw, base64, or hex"
),
to_format: str = typer.Option(
"base64", "--to", "-t", help="Target format: raw, base64, or hex"
),
):
"""Convert between different encodings."""
# First decode from source format to raw bytes
raw_bytes = data
if from_format == "base64":
try:
raw_bytes = base64.b64decode(data)
except Exception as e:
typer.echo(f"Error decoding base64: {e}", err=True)
raise typer.Exit(code=1) from e
elif from_format == "hex":
try:
raw_bytes = binascii.unhexlify(data)
except Exception as e:
typer.echo(f"Error decoding hex: {e}", err=True)
raise typer.Exit(code=1) from e
elif from_format != "raw":
typer.echo(f"Unknown source format: {from_format}", err=True)
raise typer.Exit(code=1)

# Then encode to target format
if to_format == "raw":
typer.echo(f"Raw bytes: {raw_bytes!r}")
typer.echo(f"As string: {raw_bytes.decode(errors='replace')}")
elif to_format == "base64":
encoded = base64.b64encode(raw_bytes).decode()
typer.echo(f"Base64 encoded: {encoded}")
elif to_format == "hex":
encoded = binascii.hexlify(raw_bytes).decode()
typer.echo(f"Hex encoded: {encoded}")
else:
typer.echo(f"Unknown target format: {to_format}", err=True)
raise typer.Exit(code=1)


@app.command()
def convert_latin1(
data: bytes = typer.Argument(
..., help="Data to convert (latin-1)", encoding="latin-1"
),
from_format: str = typer.Option(
"raw", "--from", "-f", help="Source format: raw, base64, or hex"
),
to_format: str = typer.Option(
"base64", "--to", "-t", help="Target format: raw, base64, or hex"
),
):
"""Convert using latin-1 input decoding for the bytes argument."""
# Reuse the same logic as convert()
raw_bytes = data
if from_format == "base64":
try:
raw_bytes = base64.b64decode(data)
except Exception as e:
typer.echo(f"Error decoding base64: {e}", err=True)
raise typer.Exit(code=1) from e
elif from_format == "hex":
try:
raw_bytes = binascii.unhexlify(data)
except Exception as e:
typer.echo(f"Error decoding hex: {e}", err=True)
raise typer.Exit(code=1) from e
elif from_format != "raw":
typer.echo(f"Unknown source format: {from_format}", err=True)
raise typer.Exit(code=1)

if to_format == "raw":
typer.echo(f"Raw bytes: {raw_bytes!r}")
typer.echo(f"As string: {raw_bytes.decode(errors='replace')}")
elif to_format == "base64":
encoded = base64.b64encode(raw_bytes).decode()
typer.echo(f"Base64 encoded: {encoded}")
elif to_format == "hex":
encoded = binascii.hexlify(raw_bytes).decode()
typer.echo(f"Hex encoded: {encoded}")
else:
typer.echo(f"Unknown target format: {to_format}", err=True)
raise typer.Exit(code=1)


@app.command()
def option_ascii_replace(
payload: bytes = typer.Option(
...,
"--payload",
help="Bytes option encoded with ascii and errors=replace",
encoding="ascii",
errors="replace",
),
):
"""Demonstrate bytes option with ascii encoding and errors=replace."""
typer.echo(f"Option bytes: {payload!r}")


if __name__ == "__main__":
app()
57 changes: 57 additions & 0 deletions examples/bytes_type_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import base64

import typer

app = typer.Typer()


@app.command()
def encode(text: bytes):
"""Encode text to base64."""
encoded = base64.b64encode(text)
typer.echo(f"Original: {text!r}")
typer.echo(f"Encoded: {encoded.decode()}")


@app.command()
def decode(encoded: str):
"""Decode base64 to bytes."""
decoded = base64.b64decode(encoded)
typer.echo(f"Encoded: {encoded}")
typer.echo(f"Decoded: {decoded!r}")


@app.command()
def echo_default(
name: bytes = typer.Argument(..., help="Name as bytes (default UTF-8)"),
):
"""Echo bytes with default UTF-8 encoding."""
typer.echo(f"Default UTF-8 bytes: {name!r}")


@app.command()
def echo_latin1(
name: bytes = typer.Argument(
..., encoding="latin-1", help="Name as bytes (latin-1)"
),
):
"""Echo bytes with latin-1 encoding for the argument."""
typer.echo(f"Latin-1 bytes: {name!r}")


@app.command()
def option_ascii_replace(
token: bytes = typer.Option(
...,
"--token",
encoding="ascii",
errors="replace",
help="Token as bytes (ascii, errors=replace)",
),
):
"""Option demonstrating ascii encoding with errors=replace."""
typer.echo(f"Option bytes: {token!r}")


if __name__ == "__main__":
app()
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ nav:
- tutorial/parameter-types/enum.md
- tutorial/parameter-types/path.md
- tutorial/parameter-types/file.md
- tutorial/parameter-types/bytes.md

- tutorial/parameter-types/custom-types.md
- SubCommands - Command Groups:
- tutorial/subcommands/index.md
Expand Down
Loading
Loading