```console
// You could create an env var MY_NAME with
$ export MY_NAME="Wade Wilson"
// Then you could use it with other programs, like
$ echo "Hello $MY_NAME"
Hello Wade Wilson
```
////
//// tab | Windows PowerShell
```console
// Create an env var MY_NAME
$ $Env:MY_NAME = "Wade Wilson"
// Use it with other programs, like
$ echo "Hello $Env:MY_NAME"
Hello Wade Wilson
```
////
## Read env vars in Python
You could also create environment variables **outside** of Python, in the terminal (or with any other method), and then **read them in Python**.
For example you could have a file `main.py` with:
```Python hl_lines="3"
import os
name = os.getenv("MY_NAME", "World")
print(f"Hello {name} from Python")
```
/// tip
The second argument to
```console
// Here we don't set the env var yet
$ python main.py
// As we didn't set the env var, we get the default value
Hello World from Python
// But if we create an environment variable first
$ export MY_NAME="Wade Wilson"
// And then call the program again
$ python main.py
// Now it can read the environment variable
Hello Wade Wilson from Python
```
////
//// tab | Windows PowerShell
```console
// Here we don't set the env var yet
$ python main.py
// As we didn't set the env var, we get the default value
Hello World from Python
// But if we create an environment variable first
$ $Env:MY_NAME = "Wade Wilson"
// And then call the program again
$ python main.py
// Now it can read the environment variable
Hello Wade Wilson from Python
```
////
As environment variables can be set outside of the code, but can be read by the code, and don't have to be stored (committed to `git`) with the rest of the files, it's common to use them for configurations or **settings**.
You can also create an environment variable only for a **specific program invocation**, that is only available to that program, and only for its duration.
To do that, create it right before the program itself, on the same line:
```console
// Create an env var MY_NAME in line for this program call
$ MY_NAME="Wade Wilson" python main.py
// Now it can read the environment variable
Hello Wade Wilson from Python
// The env var no longer exists afterwards
$ python main.py
Hello World from Python
```
/// tip
You can read more about it at
```console
$ pip install typer
---> 100%
Successfully installed typer rich shellingham
```
## Example
### The absolute minimum
* Create a file `main.py` with:
```Python
def main(name: str):
print(f"Hello {name}")
```
This script doesn't even use Typer internally. But you can use the `typer` command to run it as a CLI application.
### Run it
Run your application with the `typer` command:
```console
// Run your application
$ typer main.py run
// You get a nice error, you are missing NAME
Usage: typer [PATH_OR_MODULE] run [OPTIONS] NAME
Try 'typer [PATH_OR_MODULE] run --help' for help.
╭─ Error ───────────────────────────────────────────╮
│ Missing argument 'NAME'. │
╰───────────────────────────────────────────────────╯
// You get a --help for free
$ typer main.py run --help
Usage: typer [PATH_OR_MODULE] run [OPTIONS] NAME
Run the provided Typer app.
╭─ Arguments ───────────────────────────────────────╮
│ * name TEXT [default: None] [required] |
╰───────────────────────────────────────────────────╯
╭─ Options ─────────────────────────────────────────╮
│ --help Show this message and exit. │
╰───────────────────────────────────────────────────╯
// Now pass the NAME argument
$ typer main.py run Camila
Hello Camila
// It works! 🎉
```
This is the simplest use case, not even using Typer internally, but it can already be quite useful for simple scripts.
**Note**: auto-completion works when you create a Python package and run it with `--install-completion` or when you use the `typer` command.
## Use Typer in your code
Now let's start using Typer in your own code, update `main.py` with:
```Python
import typer
def main(name: str):
print(f"Hello {name}")
if __name__ == "__main__":
typer.run(main)
```
Now you could run it with Python directly:
```console
// Run your application
$ python main.py
// You get a nice error, you are missing NAME
Usage: main.py [OPTIONS] NAME
Try 'main.py --help' for help.
╭─ Error ───────────────────────────────────────────╮
│ Missing argument 'NAME'. │
╰───────────────────────────────────────────────────╯
// You get a --help for free
$ python main.py --help
Usage: main.py [OPTIONS] NAME
╭─ Arguments ───────────────────────────────────────╮
│ * name TEXT [default: None] [required] |
╰───────────────────────────────────────────────────╯
╭─ Options ─────────────────────────────────────────╮
│ --help Show this message and exit. │
╰───────────────────────────────────────────────────╯
// Now pass the NAME argument
$ python main.py Camila
Hello Camila
// It works! 🎉
```
**Note**: you can also call this same script with the `typer` command, but you don't need to.
## Example upgrade
This was the simplest example possible.
Now let's see one a bit more complex.
### An example with two subcommands
Modify the file `main.py`.
Create a `typer.Typer()` app, and create two subcommands with their parameters.
```Python hl_lines="3 6 11 20"
import typer
app = typer.Typer()
@app.command()
def hello(name: str):
print(f"Hello {name}")
@app.command()
def goodbye(name: str, formal: bool = False):
if formal:
print(f"Goodbye Ms. {name}. Have a good day.")
else:
print(f"Bye {name}!")
if __name__ == "__main__":
app()
```
And that will:
* Explicitly create a `typer.Typer` app.
* The previous `typer.run` actually creates one implicitly for you.
* Add two subcommands with `@app.command()`.
* Execute the `app()` itself, as if it was a function (instead of `typer.run`).
### Run the upgraded example
Check the new help:
```console
$ python main.py --help
Usage: main.py [OPTIONS] COMMAND [ARGS]...
╭─ Options ─────────────────────────────────────────╮
│ --install-completion Install completion │
│ for the current │
│ shell. │
│ --show-completion Show completion for │
│ the current shell, │
│ to copy it or │
│ customize the │
│ installation. │
│ --help Show this message │
│ and exit. │
╰───────────────────────────────────────────────────╯
╭─ Commands ────────────────────────────────────────╮
│ goodbye │
│ hello │
╰───────────────────────────────────────────────────╯
// When you create a package you get ✨ auto-completion ✨ for free, installed with --install-completion
// You have 2 subcommands (the 2 functions): goodbye and hello
```
Now check the help for the `hello` command:
```console
$ python main.py hello --help
Usage: main.py hello [OPTIONS] NAME
╭─ Arguments ───────────────────────────────────────╮
│ * name TEXT [default: None] [required] │
╰───────────────────────────────────────────────────╯
╭─ Options ─────────────────────────────────────────╮
│ --help Show this message and exit. │
╰───────────────────────────────────────────────────╯
```
And now check the help for the `goodbye` command:
```console
$ python main.py goodbye --help
Usage: main.py goodbye [OPTIONS] NAME
╭─ Arguments ───────────────────────────────────────╮
│ * name TEXT [default: None] [required] │
╰───────────────────────────────────────────────────╯
╭─ Options ─────────────────────────────────────────╮
│ --formal --no-formal [default: no-formal] │
│ --help Show this message │
│ and exit. │
╰───────────────────────────────────────────────────╯
// Automatic --formal and --no-formal for the bool option 🎉
```
Now you can try out the new command line application:
```console
// Use it with the hello command
$ python main.py hello Camila
Hello Camila
// And with the goodbye command
$ python main.py goodbye Camila
Bye Camila!
// And with --formal
$ python main.py goodbye --formal Camila
Goodbye Ms. Camila. Have a good day.
```
**Note**: If your app only has one command, by default the command name is **omitted** in usage: `python main.py Camila`. However, when there are multiple commands, you must **explicitly include the command name**: `python main.py hello Camila`. See [One or Multiple Commands](https://typer.tiangolo.com/tutorial/commands/one-or-multiple/) for more details.
### Recap
In summary, you declare **once** the types of parameters (*CLI arguments* and *CLI options*) as function parameters.
You do that with standard modern Python types.
You don't have to learn a new syntax, the methods or classes of a specific library, etc.
Just standard **Python**.
For example, for an `int`:
```Python
total: int
```
or for a `bool` flag:
```Python
force: bool
```
And similarly for **files**, **paths**, **enums** (choices), etc. And there are tools to create **groups of subcommands**, add metadata, extra **validation**, etc.
**You get**: great editor support, including **completion** and **type checks** everywhere.
**Your users get**: automatic **`--help`**, **auto-completion** in their terminal (Bash, Zsh, Fish, PowerShell) when they install your package or when using the `typer` command.
For a more complete example including more features, see the
* @version 0.0.1
* @license MIT
*/
'use strict';
/** Generate a terminal widget. */
class Termynal {
/**
* Construct the widget's settings.
* @param {(string|Node)=} container - Query selector or container element.
* @param {Object=} options - Custom settings.
* @param {string} options.prefix - Prefix to use for data attributes.
* @param {number} options.startDelay - Delay before animation, in ms.
* @param {number} options.typeDelay - Delay between each typed character, in ms.
* @param {number} options.lineDelay - Delay between each line, in ms.
* @param {number} options.progressLength - Number of characters displayed as progress bar.
* @param {string} options.progressChar – Character to use for progress bar, defaults to █.
* @param {number} options.progressPercent - Max percent of progress.
* @param {string} options.cursor – Character to use for cursor, defaults to ▋.
* @param {Object[]} lineData - Dynamically loaded line data objects.
* @param {boolean} options.noInit - Don't initialise the animation.
*/
constructor(container = '#termynal', options = {}) {
this.container = (typeof container === 'string') ? document.querySelector(container) : container;
this.pfx = `data-${options.prefix || 'ty'}`;
this.originalStartDelay = this.startDelay = options.startDelay
|| parseFloat(this.container.getAttribute(`${this.pfx}-startDelay`)) || 600;
this.originalTypeDelay = this.typeDelay = options.typeDelay
|| parseFloat(this.container.getAttribute(`${this.pfx}-typeDelay`)) || 90;
this.originalLineDelay = this.lineDelay = options.lineDelay
|| parseFloat(this.container.getAttribute(`${this.pfx}-lineDelay`)) || 1500;
this.progressLength = options.progressLength
|| parseFloat(this.container.getAttribute(`${this.pfx}-progressLength`)) || 40;
this.progressChar = options.progressChar
|| this.container.getAttribute(`${this.pfx}-progressChar`) || '█';
this.progressPercent = options.progressPercent
|| parseFloat(this.container.getAttribute(`${this.pfx}-progressPercent`)) || 100;
this.cursor = options.cursor
|| this.container.getAttribute(`${this.pfx}-cursor`) || '▋';
this.lineData = this.lineDataToElements(options.lineData || []);
this.loadLines()
if (!options.noInit) this.init()
}
loadLines() {
// Load all the lines and create the container so that the size is fixed
// Otherwise it would be changing and the user viewport would be constantly
// moving as she/he scrolls
const finish = this.generateFinish()
finish.style.visibility = 'hidden'
this.container.appendChild(finish)
// Appends dynamically loaded lines to existing line elements.
this.lines = [...this.container.querySelectorAll(`[${this.pfx}]`)].concat(this.lineData);
for (let line of this.lines) {
line.style.visibility = 'hidden'
this.container.appendChild(line)
}
const restart = this.generateRestart()
restart.style.visibility = 'hidden'
this.container.appendChild(restart)
this.container.setAttribute('data-termynal', '');
}
/**
* Initialise the widget, get lines, clear container and start animation.
*/
init() {
/**
* Calculates width and height of Termynal container.
* If container is empty and lines are dynamically loaded, defaults to browser `auto` or CSS.
*/
const containerStyle = getComputedStyle(this.container);
this.container.style.width = containerStyle.width !== '0px' ?
containerStyle.width : undefined;
this.container.style.minHeight = containerStyle.height !== '0px' ?
containerStyle.height : undefined;
this.container.setAttribute('data-termynal', '');
this.container.innerHTML = '';
for (let line of this.lines) {
line.style.visibility = 'visible'
}
this.start();
}
/**
* Start the animation and rener the lines depending on their data attributes.
*/
async start() {
this.addFinish()
await this._wait(this.startDelay);
for (let line of this.lines) {
const type = line.getAttribute(this.pfx);
const delay = line.getAttribute(`${this.pfx}-delay`) || this.lineDelay;
if (type == 'input') {
line.setAttribute(`${this.pfx}-cursor`, this.cursor);
await this.type(line);
await this._wait(delay);
}
else if (type == 'progress') {
await this.progress(line);
await this._wait(delay);
}
else {
this.container.appendChild(line);
await this._wait(delay);
}
line.removeAttribute(`${this.pfx}-cursor`);
}
this.addRestart()
this.finishElement.style.visibility = 'hidden'
this.lineDelay = this.originalLineDelay
this.typeDelay = this.originalTypeDelay
this.startDelay = this.originalStartDelay
}
generateRestart() {
const restart = document.createElement('a')
restart.onclick = (e) => {
e.preventDefault()
this.container.innerHTML = ''
this.init()
}
restart.href = '#'
restart.setAttribute('data-terminal-control', '')
restart.innerHTML = "restart ↻"
return restart
}
generateFinish() {
const finish = document.createElement('a')
finish.onclick = (e) => {
e.preventDefault()
this.lineDelay = 0
this.typeDelay = 0
this.startDelay = 0
}
finish.href = '#'
finish.setAttribute('data-terminal-control', '')
finish.innerHTML = "fast →"
this.finishElement = finish
return finish
}
addRestart() {
const restart = this.generateRestart()
this.container.appendChild(restart)
}
addFinish() {
const finish = this.generateFinish()
this.container.appendChild(finish)
}
/**
* Animate a typed line.
* @param {Node} line - The line element to render.
*/
async type(line) {
const chars = [...line.textContent];
line.textContent = '';
this.container.appendChild(line);
for (let char of chars) {
const delay = line.getAttribute(`${this.pfx}-typeDelay`) || this.typeDelay;
await this._wait(delay);
line.textContent += char;
}
}
/**
* Animate a progress bar.
* @param {Node} line - The line element to render.
*/
async progress(line) {
const progressLength = line.getAttribute(`${this.pfx}-progressLength`)
|| this.progressLength;
const progressChar = line.getAttribute(`${this.pfx}-progressChar`)
|| this.progressChar;
const chars = progressChar.repeat(progressLength);
const progressPercent = line.getAttribute(`${this.pfx}-progressPercent`)
|| this.progressPercent;
line.textContent = '';
this.container.appendChild(line);
for (let i = 1; i < chars.length + 1; i++) {
await this._wait(this.typeDelay);
const percent = Math.round(i / chars.length * 100);
line.textContent = `${chars.slice(0, i)} ${percent}%`;
if (percent>progressPercent) {
break;
}
}
}
/**
* Helper function for animation delays, called with `await`.
* @param {number} time - Timeout, in ms.
*/
_wait(time) {
return new Promise(resolve => setTimeout(resolve, time));
}
/**
* Converts line data objects into line elements.
*
* @param {Object[]} lineData - Dynamically loaded lines.
* @param {Object} line - Line data object.
* @returns {Element[]} - Array of line elements.
*/
lineDataToElements(lineData) {
return lineData.map(line => {
let div = document.createElement('div');
div.innerHTML = `${line.value || ''}`;
return div.firstElementChild;
});
}
/**
* Helper function for generating attributes string.
*
* @param {Object} line - Line data object.
* @returns {string} - String of attributes.
*/
_attributes(line) {
let attrs = '';
for (let prop in line) {
// Custom add class
if (prop === 'class') {
attrs += ` class=${line[prop]} `
continue
}
if (prop === 'type') {
attrs += `${this.pfx}="${line[prop]}" `
} else if (prop !== 'value') {
attrs += `${this.pfx}-${prop}="${line[prop]}" `
}
}
return attrs;
}
}
/**
* HTML API: If current script has container(s) specified, initialise Termynal.
*/
if (document.currentScript.hasAttribute('data-termynal-container')) {
const containers = document.currentScript.getAttribute('data-termynal-container');
containers.split('|')
.forEach(container => new Termynal(container))
}
================================================
FILE: docs/management-tasks.md
================================================
# Repository Management Tasks
These are the tasks that can be performed to manage the Typer repository by [team members](./management.md#team){.internal-link target=_blank}.
/// tip
This section is useful only to a handful of people, team members with permissions to manage the repository. You can probably skip it. 😉
///
...so, you are a [team member of Typer](./management.md#team){.internal-link target=_blank}? Wow, you are so cool! 😎
You can help with everything on [Help Typer - Get Help](./help-typer.md){.internal-link target=_blank} the same ways as external contributors. But additionally, there are some tasks that only you (as part of the team) can perform.
Here are the general instructions for the tasks you can perform.
Thanks a lot for your help. 🙇
## Be Nice
First of all, be nice. 😊
You probably are super nice if you were added to the team, but it's worth mentioning it. 🤓
### When Things are Difficult
When things are great, everything is easier, so that doesn't need much instructions. But when things are difficult, here are some guidelines.
Try to find the good side. In general, if people are not being unfriendly, try to thank their effort and interest, even if you disagree with the main subject (discussion, PR), just thank them for being interested in the project, or for having dedicated some time to try to do something.
It's difficult to convey emotion in text, use emojis to help. 😅
In discussions and PRs, in many cases, people bring their frustration and show it without filter, in many cases exaggerating, complaining, being entitled, etc. That's really not nice, and when it happens, it lowers our priority to solve their problems. But still, try to breath, and be gentle with your answers.
Try to avoid using bitter sarcasm or potentially passive-aggressive comments. If something is wrong, it's better to be direct (try to be gentle) than sarcastic.
Try to be as specific and objective as possible, avoid generalizations.
For conversations that are more difficult, for example to reject a PR, you can ask me (@tiangolo) to handle it directly.
## Edit PR Titles
* Edit the PR title to start with an emoji from gitmoji.
* Use the emoji character, not the GitHub code. So, use `🐛` instead of `:bug:`. This is so that it shows up correctly outside of GitHub, for example in the release notes.
* Start the title with a verb. For example `Add`, `Refactor`, `Fix`, etc. This way the title will say the action that the PR does. Like `Add support for teleporting`, instead of `Teleporting wasn't working, so this PR fixes it`.
* Edit the text of the PR title to start in "imperative", like giving an order. So, instead of `Adding support for teleporting` use `Add support for teleporting`.
* Try to make the title descriptive about what it achieves. If it's a feature, try to describe it, for example `Add support for teleporting` instead of `Create TeleportAdapter class`.
* Do not finish the title with a period (`.`).
Once the PR is merged, a GitHub Action (latest-changes) will use the PR title to update the latest changes automatically.
So, having a nice PR title will not only look nice in GitHub, but also in the release notes. 📝
## Add Labels to PRs
The same GitHub Action latest-changes uses one label in the PR to decide the section in the release notes to put this PR in.
Make sure you use a supported label from the latest-changes list of labels:
* `breaking`: Breaking Changes
* Existing code will break if they update the version without changing their code. This rarely happens, so this label is not frequently used.
* `security`: Security Fixes
* This is for security fixes, like vulnerabilities. It would almost never be used.
* `feature`: Features
* New features, adding support for things that didn't exist before.
* `bug`: Fixes
* Something that was supported didn't work, and this fixes it. There are many PRs that claim to be bug fixes because the user is doing something in an unexpected way that is not supported, but they considered it what should be supported by default. Many of these are actually features or refactors. But in some cases there's an actual bug.
* `refactor`: Refactors
* This is normally for changes to the internal code that don't change the behavior. Normally it improves maintainability, or enables future features, etc.
* `upgrade`: Upgrades
* This is for upgrades to direct dependencies from the project, or extra optional dependencies, normally in `pyproject.toml`. So, things that would affect final users, they would end up receiving the upgrade in their code base once they update. But this is not for upgrades to internal dependencies used for development, testing, docs, etc. Those internal dependencies or GitHub Action versions should be marked as `internal`, not `upgrade`.
* `docs`: Docs
* Changes in docs. This includes updating the docs, fixing typos. But it doesn't include changes to translations.
* You can normally quickly detect it by going to the "Files changed" tab in the PR and checking if the updated file(s) starts with `docs/en/docs`. The original version of the docs is always in English, so in `docs/en/docs`.
* `internal`: Internal
* Use this for changes that only affect how the repo is managed. For example upgrades to internal dependencies, changes in GitHub Actions or scripts, etc.
/// tip
Some tools like Dependabot, will add some labels, like `dependencies`, but have in mind that this label is not used by the `latest-changes` GitHub Action, so it won't be used in the release notes. Please make sure one of the labels above is added.
///
## Review PRs
* If a PR doesn't explain what it does or why, if it seems like it could be useful, ask for more information. Otherwise, feel free to close it.
* If a PR seems to be spam, meaningless, only to change statistics (to appear as "contributor") or similar, you can simply mark it as `invalid`, and it will be automatically closed.
* If a PR seems to be AI generated, and seems like reviewing it would take more time from you than the time it took to write the prompt, mark it as `maybe-ai`, and it will be automatically closed.
* A PR should have a specific use case that it is solving.
* If the PR is for a feature, it should have docs.
* Unless it's a feature we want to discourage, like support for a corner case that we don't want users to use.
* The docs should include a source example file, not write Python directly in Markdown.
* If the source example(s) file can have different syntax for different Python versions, there should be different versions of the file, and they should be shown in tabs in the docs.
* There should be tests testing the source example.
* Before the PR is applied, the new tests should fail.
* After applying the PR, the new tests should pass.
* Coverage should stay at 100%.
* If you see the PR makes sense, or we discussed it and considered it should be accepted, you can add commits on top of the PR to tweak it, to add docs, tests, format, refactor, remove extra files, etc.
* Feel free to comment in the PR to ask for more information, to suggest changes, etc.
* Once you think the PR is ready, move it in the internal GitHub project for me to review it.
## Dependabot PRs
Dependabot will create PRs to update dependencies for several things, and those PRs all look similar, but some are way more delicate than others.
* If the PR is for a direct dependency, so, Dependabot is modifying `pyproject.toml` in the main dependencies, **don't merge it**. 😱 Let me check it first. There's a good chance that some additional tweaks or updates are needed.
* If the PR updates one of the internal dependencies, for example the group `dev` in `pyproject.toml`, or GitHub Action versions, if the tests are passing, the release notes (shown in a summary in the PR) don't show any obvious potential breaking change, you can merge it. 😎
## Mark GitHub Discussions Answers
When a question in GitHub Discussions has been answered, mark the answer by clicking "Mark as answer".
You can filter discussions by `Questions` that are `Unanswered`.
================================================
FILE: docs/management.md
================================================
# Repository Management
Here's a short description of how the Typer repository is managed and maintained.
## Owner
I, @tiangolo, am the creator and owner of the Typer repository. 🤓
I normally give the final review to each PR before merging them. I make the final decisions on the project, I'm the BDFL. 😅
## Team
There's a team of people that help manage and maintain the project. 😎
They have different levels of permissions and [specific instructions](./management-tasks.md){.internal-link target=_blank}.
Some of the tasks they can perform include:
* Adding labels to PRs.
* Editing PR titles.
* Adding commits on top of PRs to tweak them.
* Mark answers in GitHub Discussions questions, etc.
* Merge some specific types of PRs.
Joining the team is by invitation only, and I could update or remove permissions, instructions, or membership.
### Team Members
This is the current list of team members. 😎
{% for user in members["members"] %}
{% endfor %}
Additional to them, there's a large community of people helping each other and getting involved in the projects in different ways.
## External Contributions
External contributions are very welcome and appreciated, including answering questions, submitting PRs, etc. 🙇♂️
There are many ways to [help maintain Typer](./help-typer.md){.internal-link target=_blank}.
================================================
FILE: docs/overrides/main.html
================================================
{% extends "base.html" %}
================================================
FILE: docs/reference/context.md
================================================
# Context
Every app has a special internal object that keeps track of state relevant to the script's execution.
For some advanced use-cases, you may want to access it directly.
This can be done by declaring a function parameter of type `typer.Context`.
Similarly, you can also declare a function parameter with type `typer.CallbackParam` in case a callback could be used
by several CLI parameters, and you need to figure out which one it was.
```python
from typing import Annotated
import typer
app = typer.Typer()
def name_callback(ctx: typer.Context, param: typer.CallbackParam, value: str):
if ctx.resilient_parsing:
return
print(f"Validating param: {param.name}")
if value != "Rick":
raise typer.BadParameter("Only Rick is allowed")
return value
@app.command()
def main(name: Annotated[str | None, typer.Option(callback=name_callback)] = None):
print(f"Hello {name}")
if __name__ == "__main__":
app()
```
::: typer.Context
::: typer.CallbackParam
================================================
FILE: docs/reference/file_objects.md
================================================
# File objects
When you want to declare some types of files, you can use `Path`.
However, in some cases you may need to have access to a file-like object, and then you can use these [special File types](https://typer.tiangolo.com/tutorial/parameter-types/file/).
These objects can be imported from `typer` directly:
```python
from typer import FileBinaryRead, FileBinaryWrite, FileText, FileTextWrite
```
::: typer.FileText
::: typer.FileTextWrite
::: typer.FileBinaryRead
::: typer.FileBinaryWrite
================================================
FILE: docs/reference/index.md
================================================
# Reference
Here's the reference or code API, the classes, functions, parameters, attributes, and
all the Typer parts you can use in your applications.
If you want to **learn Typer** you are much better off reading the
[Typer Tutorial](https://typer.tiangolo.com/tutorial/).
================================================
FILE: docs/reference/parameters.md
================================================
# Parameters
Parameters to our command line interface may be both [CLI Options](https://typer.tiangolo.com/tutorial/options/) and [CLI Arguments](https://typer.tiangolo.com/tutorial/arguments/):
```python
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def register(
user: Annotated[str, typer.Argument()],
age: Annotated[int, typer.Option(min=18)],
score: Annotated[float, typer.Option(max=100)] = 0,
):
print(f"User is {user}")
print(f"--age is {age}")
print(f"--score is {score}")
if __name__ == "__main__":
app()
```
::: typer.Argument
options:
show_overloads: false
::: typer.Option
options:
show_overloads: false
================================================
FILE: docs/reference/run_launch.md
================================================
# `run` and `launch`
These two functions can be imported from `typer` directly:
```python
from typer import launch, run
```
::: typer.run
::: typer.launch
================================================
FILE: docs/reference/typer.md
================================================
# `Typer` class
Here's the reference information for the `Typer` class, with all its parameters, attributes and methods.
You can import the `Typer` class directly from `typer`:
```python
from typer import Typer
```
::: typer.Typer
options:
members:
- callback
- command
- add_typer
================================================
FILE: docs/release-notes.md
================================================
# Release Notes
## Latest Changes
### Refactors
* 🎨 Ensure `ty` runs without errors. PR [#1628](https://github.com/fastapi/typer/pull/1628) by [@svlandeg](https://github.com/svlandeg).
### Docs
* 📝 Fix broken link to FastAPI and Friends newsletter. PR [#1540](https://github.com/fastapi/typer/pull/1540) by [@Karlemami](https://github.com/Karlemami).
* 🔨 Handle external links `target=_blank` and CSS automatically in JS and CSS. PR [#1622](https://github.com/fastapi/typer/pull/1622) by [@tiangolo](https://github.com/tiangolo).
* 📝 Remove link to Typer developer survey. PR [#1609](https://github.com/fastapi/typer/pull/1609) by [@svlandeg](https://github.com/svlandeg).
* ✏️ Clean up documentation in `install.md` file. PR [#1606](https://github.com/fastapi/typer/pull/1606) by [@Johandielangman](https://github.com/Johandielangman).
### Internal
* ⬆ Bump prek from 0.3.5 to 0.3.6. PR [#1638](https://github.com/fastapi/typer/pull/1638) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump ty from 0.0.22 to 0.0.23. PR [#1639](https://github.com/fastapi/typer/pull/1639) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump pyjwt from 2.10.1 to 2.12.0. PR [#1636](https://github.com/fastapi/typer/pull/1636) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump cairosvg from 2.8.2 to 2.9.0. PR [#1635](https://github.com/fastapi/typer/pull/1635) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump ruff from 0.15.5 to 0.15.6. PR [#1633](https://github.com/fastapi/typer/pull/1633) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump ty from 0.0.21 to 0.0.22. PR [#1634](https://github.com/fastapi/typer/pull/1634) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump dorny/paths-filter from 3 to 4. PR [#1632](https://github.com/fastapi/typer/pull/1632) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump mkdocs-material from 9.7.4 to 9.7.5. PR [#1629](https://github.com/fastapi/typer/pull/1629) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump prek from 0.3.4 to 0.3.5. PR [#1627](https://github.com/fastapi/typer/pull/1627) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump ty from 0.0.20 to 0.0.21. PR [#1624](https://github.com/fastapi/typer/pull/1624) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump ruff from 0.15.4 to 0.15.5. PR [#1625](https://github.com/fastapi/typer/pull/1625) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump mkdocs-material from 9.7.3 to 9.7.4. PR [#1621](https://github.com/fastapi/typer/pull/1621) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump ty from 0.0.19 to 0.0.20. PR [#1618](https://github.com/fastapi/typer/pull/1618) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump prek from 0.3.3 to 0.3.4. PR [#1617](https://github.com/fastapi/typer/pull/1617) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump ruff from 0.15.2 to 0.15.4. PR [#1616](https://github.com/fastapi/typer/pull/1616) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump ty from 0.0.18 to 0.0.19. PR [#1615](https://github.com/fastapi/typer/pull/1615) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump actions/download-artifact from 7 to 8. PR [#1614](https://github.com/fastapi/typer/pull/1614) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump actions/upload-artifact from 6 to 7. PR [#1613](https://github.com/fastapi/typer/pull/1613) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump mkdocs-material from 9.7.2 to 9.7.3. PR [#1611](https://github.com/fastapi/typer/pull/1611) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump mkdocstrings-python from 2.0.1 to 2.0.3. PR [#1605](https://github.com/fastapi/typer/pull/1605) by [@svlandeg](https://github.com/svlandeg).
* ⬆ Bump ty from 0.0.17 to 0.0.18. PR [#1602](https://github.com/fastapi/typer/pull/1602) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump griffe-warnings-deprecated from 1.1.0 to 1.1.1. PR [#1603](https://github.com/fastapi/typer/pull/1603) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump griffe-typingdoc from 0.3.0 to 0.3.1. PR [#1604](https://github.com/fastapi/typer/pull/1604) by [@dependabot[bot]](https://github.com/apps/dependabot).
* 🔧 Upgrade pytest version and config. PR [#1570](https://github.com/fastapi/typer/pull/1570) by [@tiangolo](https://github.com/tiangolo).
## 0.24.1
### Internal
* 👷 Fix CI, do not attempt to build `typer-slim`, nor `typer-cli`. PR [#1569](https://github.com/fastapi/typer/pull/1569) by [@tiangolo](https://github.com/tiangolo).
* ➖ Drop support for `typer-slim` and `typer-cli`, no more versions will be released, use only `typer`. PR [#1568](https://github.com/fastapi/typer/pull/1568) by [@tiangolo](https://github.com/tiangolo).
* ⬆ Bump rich from 14.3.2 to 14.3.3. PR [#1565](https://github.com/fastapi/typer/pull/1565) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump pydantic-settings from 2.13.0 to 2.13.1. PR [#1566](https://github.com/fastapi/typer/pull/1566) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump ruff from 0.15.1 to 0.15.2. PR [#1567](https://github.com/fastapi/typer/pull/1567) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump mkdocs-material from 9.7.1 to 9.7.2. PR [#1561](https://github.com/fastapi/typer/pull/1561) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump pillow from 11.3.0 to 12.1.1. PR [#1550](https://github.com/fastapi/typer/pull/1550) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump pytest from 8.4.2 to 9.0.2. PR [#1551](https://github.com/fastapi/typer/pull/1551) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump pydantic-settings from 2.12.0 to 2.13.0. PR [#1552](https://github.com/fastapi/typer/pull/1552) by [@dependabot[bot]](https://github.com/apps/dependabot).
* 🎨 Upgrade types for Python 3.10. PR [#1549](https://github.com/fastapi/typer/pull/1549) by [@tiangolo](https://github.com/tiangolo).
* 🔨 Add internal scripts to migrate docs from Python 3.9 to 3.10. PR [#1547](https://github.com/fastapi/typer/pull/1547) by [@tiangolo](https://github.com/tiangolo).
* ⬆ Bump ty from 0.0.16 to 0.0.17. PR [#1544](https://github.com/fastapi/typer/pull/1544) by [@dependabot[bot]](https://github.com/apps/dependabot).
## 0.24.0
### Breaking Changes
* ➖ Drop support for Python 3.9. PR [#1546](https://github.com/fastapi/typer/pull/1546) by [@tiangolo](https://github.com/tiangolo).
## 0.23.2
### Features
* ✅ Monkeypatch console width to allow running `pytest` directly. PR [#1542](https://github.com/fastapi/typer/pull/1542) by [@SwaatiR](https://github.com/SwaatiR).
### Internal
* 👷 Run tests with lower bound uv sync, update minimum dependencies. PR [#1526](https://github.com/fastapi/typer/pull/1526) by [@YuriiMotov](https://github.com/YuriiMotov).
* ⬆ Bump prek from 0.3.2 to 0.3.3. PR [#1545](https://github.com/fastapi/typer/pull/1545) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump ruff from 0.15.0 to 0.15.1. PR [#1541](https://github.com/fastapi/typer/pull/1541) by [@dependabot[bot]](https://github.com/apps/dependabot).
## 0.23.1
### Fixes
* 🐛 Fix `TYPER_USE_RICH` parsing to allow disabling Rich completely. PR [#1539](https://github.com/fastapi/typer/pull/1539) by [@bckohan](https://github.com/bckohan).
### Docs
* 📝 Remove documentation pages that reference using Click directly. PR [#1538](https://github.com/fastapi/typer/pull/1538) by [@svlandeg](https://github.com/svlandeg).
### Internal
* ⬆ Bump ty from 0.0.15 to 0.0.16. PR [#1533](https://github.com/fastapi/typer/pull/1533) by [@dependabot[bot]](https://github.com/apps/dependabot).
## 0.23.0
### Breaking Changes
* ♻️ When printing error tracebacks with Rich, default to not showing locals, which are sometimes verbose. PR [#1072](https://github.com/fastapi/typer/pull/1072) by [@tiangolo](https://github.com/tiangolo).
### Docs
* 📝 Add more explicit deprecation note in shell packages. PR [#1534](https://github.com/fastapi/typer/pull/1534) by [@tiangolo](https://github.com/tiangolo).
### Internal
* ⬆ Bump cryptography from 46.0.4 to 46.0.5. PR [#1532](https://github.com/fastapi/typer/pull/1532) by [@dependabot[bot]](https://github.com/apps/dependabot).
* 🔨 Tweak PDM hook script, remove unnecessary default. PR [#1536](https://github.com/fastapi/typer/pull/1536) by [@tiangolo](https://github.com/tiangolo).
* ♻️ Simplify build setup scripts and configs for deprecated wrapper packages. PR [#1535](https://github.com/fastapi/typer/pull/1535) by [@tiangolo](https://github.com/tiangolo).
## 0.22.0
### Breaking Changes
* 💥 Make `typer-slim` a shallow wrapper around `typer`, always requiring `rich` and `shellingham`. PR [#1522](https://github.com/fastapi/typer/pull/1522) by [@svlandeg](https://github.com/svlandeg).
## 0.21.2
### Fixes
* 🐛 Fix highlighting of optional variadic argument metavars. PR [#1508](https://github.com/fastapi/typer/pull/1508) by [@BenjyWiener](https://github.com/BenjyWiener).
* 🐛 Fix `--help` text alignment when using `typer.style()` in option descriptions. PR [#1356](https://github.com/fastapi/typer/pull/1356) by [@mahimairaja](https://github.com/mahimairaja).
### Refactors
* 🎨 Update types and errors, comment ty for now. PR [#1531](https://github.com/fastapi/typer/pull/1531) by [@tiangolo](https://github.com/tiangolo).
### Upgrades
* ➖ Drop `typing-extensions` as external dependency. PR [#1467](https://github.com/fastapi/typer/pull/1467) by [@svlandeg](https://github.com/svlandeg).
### Docs
* 📝 Add reference (code API) docs. PR [#1504](https://github.com/fastapi/typer/pull/1504) by [@svlandeg](https://github.com/svlandeg).
* 📝 Update the "Building a Package" tutorial to use `uv` instead of `poetry`. PR [#1474](https://github.com/fastapi/typer/pull/1474) by [@svlandeg](https://github.com/svlandeg).
* 📝 Update `management-tasks.md` to be in line with `management-tasks.md` in FastAPI repo. PR [#1519](https://github.com/fastapi/typer/pull/1519) by [@YuriiMotov](https://github.com/YuriiMotov).
* 📝 Add link to Typer developer survey. PR [#1514](https://github.com/fastapi/typer/pull/1514) by [@tiangolo](https://github.com/tiangolo).
* 📝 Add contribution instructions about LLM generated code and comments and automated tools for PRs. PR [#1489](https://github.com/fastapi/typer/pull/1489) by [@alejsdev](https://github.com/alejsdev).
* 🐛 Fix copy button in `custom.js`. PR [#1488](https://github.com/fastapi/typer/pull/1488) by [@alejsdev](https://github.com/alejsdev).
### Internal
* 🔧 Update config for labeler. PR [#1530](https://github.com/fastapi/typer/pull/1530) by [@tiangolo](https://github.com/tiangolo).
* 🔧 Remove no longer used requirements.txt file. PR [#1528](https://github.com/fastapi/typer/pull/1528) by [@tiangolo](https://github.com/tiangolo).
* 📌 Update internal dependency limits. PR [#1529](https://github.com/fastapi/typer/pull/1529) by [@tiangolo](https://github.com/tiangolo).
* 🔧 Add ty to pre-commit. PR [#1527](https://github.com/fastapi/typer/pull/1527) by [@tiangolo](https://github.com/tiangolo).
* 👷 Add `ty` to the CI `lint` check. PR [#1477](https://github.com/fastapi/typer/pull/1477) by [@svlandeg](https://github.com/svlandeg).
* ⬆ Bump prek from 0.3.1 to 0.3.2. PR [#1524](https://github.com/fastapi/typer/pull/1524) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump ruff from 0.14.14 to 0.15.0. PR [#1516](https://github.com/fastapi/typer/pull/1516) by [@dependabot[bot]](https://github.com/apps/dependabot).
* 🔧 Add generate-readme to pre-commit. PR [#1515](https://github.com/fastapi/typer/pull/1515) by [@tiangolo](https://github.com/tiangolo).
* 👷 Run mypy in pre-commit instead of `Lint` step in `test.yml` workflow. PR [#1511](https://github.com/fastapi/typer/pull/1511) by [@YuriiMotov](https://github.com/YuriiMotov).
* ⬆ Bump prek from 0.3.0 to 0.3.1. PR [#1510](https://github.com/fastapi/typer/pull/1510) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump rich from 14.3.1 to 14.3.2. PR [#1509](https://github.com/fastapi/typer/pull/1509) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ✅ Update test to use `mod` fixture. PR [#1506](https://github.com/fastapi/typer/pull/1506) by [@svlandeg](https://github.com/svlandeg).
* ⬆ Bump rich from 14.2.0 to 14.3.1. PR [#1502](https://github.com/fastapi/typer/pull/1502) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump ruff from 0.14.13 to 0.14.14. PR [#1499](https://github.com/fastapi/typer/pull/1499) by [@dependabot[bot]](https://github.com/apps/dependabot).
* 🔧 Ensure that an edit to `uv.lock` gets the `internal` label. PR [#1497](https://github.com/fastapi/typer/pull/1497) by [@svlandeg](https://github.com/svlandeg).
* ⬆ Bump ruff from 0.14.10 to 0.14.13. PR [#1493](https://github.com/fastapi/typer/pull/1493) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump urllib3 from 2.6.2 to 2.6.3. PR [#1496](https://github.com/fastapi/typer/pull/1496) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump prek from 0.2.24 to 0.3.0. PR [#1495](https://github.com/fastapi/typer/pull/1495) by [@dependabot[bot]](https://github.com/apps/dependabot).
## 0.21.1
### Fixes
* 🐛 Fix escaping in help text when `rich` is installed but not used. PR [#1089](https://github.com/fastapi/typer/pull/1089) by [@svlandeg](https://github.com/svlandeg).
### Internal
* ⬆️ Migrate to uv. PR [#1472](https://github.com/fastapi/typer/pull/1472) by [@DoctorJohn](https://github.com/DoctorJohn).
* ⬆ Bump mypy from 1.18.2 to 1.19.1. PR [#1469](https://github.com/fastapi/typer/pull/1469) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump actions/checkout from 5 to 6. PR [#1456](https://github.com/fastapi/typer/pull/1456) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump actions/download-artifact from 6 to 7. PR [#1444](https://github.com/fastapi/typer/pull/1444) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump ruff from 0.14.8 to 0.14.10. PR [#1449](https://github.com/fastapi/typer/pull/1449) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump mkdocs-material from 9.7.0 to 9.7.1. PR [#1446](https://github.com/fastapi/typer/pull/1446) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump actions/upload-artifact from 5 to 6. PR [#1443](https://github.com/fastapi/typer/pull/1443) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump actions/cache from 4 to 5. PR [#1441](https://github.com/fastapi/typer/pull/1441) by [@dependabot[bot]](https://github.com/apps/dependabot).
* 👷 Update secrets check. PR [#1471](https://github.com/fastapi/typer/pull/1471) by [@YuriiMotov](https://github.com/YuriiMotov).
* ✅ Add missing tests for code examples. PR [#1465](https://github.com/fastapi/typer/pull/1465) by [@YuriiMotov](https://github.com/YuriiMotov).
* 🔧 Update pre-commit to use local Ruff instead of hook, unpin `prek`. PR [#1466](https://github.com/fastapi/typer/pull/1466) by [@YuriiMotov](https://github.com/YuriiMotov).
* ⬆ Bump mypy from 1.14.1 to 1.18.2. PR [#1382](https://github.com/fastapi/typer/pull/1382) by [@dependabot[bot]](https://github.com/apps/dependabot).
## 0.21.0
### Breaking Changes
* ➖ Drop support for Python 3.8. PR [#1464](https://github.com/fastapi/typer/pull/1464) by [@tiangolo](https://github.com/tiangolo).
* ➖ Drop support for Python 3.8 in CI. PR [#1463](https://github.com/fastapi/typer/pull/1463) by [@YuriiMotov](https://github.com/YuriiMotov) and [@tiangolo](https://github.com/tiangolo).
### Docs
* 📝 Update code examples to Python 3.9. PR [#1459](https://github.com/fastapi/typer/pull/1459) by [@YuriiMotov](https://github.com/YuriiMotov).
### Internal
* 💚 Move `ruff` dependency to shared `requirements-docs-tests.txt` to fix "Build docs" workflow in CI. PR [#1458](https://github.com/fastapi/typer/pull/1458) by [@YuriiMotov](https://github.com/YuriiMotov).
* ⬆ Bump markdown-include-variants from 0.0.5 to 0.0.8. PR [#1442](https://github.com/fastapi/typer/pull/1442) by [@dependabot[bot]](https://github.com/apps/dependabot).
* 👷 Add pre-commit workflow. PR [#1453](https://github.com/fastapi/typer/pull/1453) by [@tiangolo](https://github.com/tiangolo).
* 👷 Configure coverage, error on main tests, don't wait for Smokeshow. PR [#1448](https://github.com/fastapi/typer/pull/1448) by [@YuriiMotov](https://github.com/YuriiMotov).
* 👷 Run Smokeshow always, even on test failures. PR [#1447](https://github.com/fastapi/typer/pull/1447) by [@YuriiMotov](https://github.com/YuriiMotov).
* 🔨 Add Typer script to generate example variants for Python files. PR [#1452](https://github.com/fastapi/typer/pull/1452) by [@tiangolo](https://github.com/tiangolo).
## 0.20.1
### Features
* ✨ Add support for standard tracebacks via the env `TYPER_STANDARD_TRACEBACK`. PR [#1299](https://github.com/fastapi/typer/pull/1299) by [@colin-nolan](https://github.com/colin-nolan).
### Fixes
* 🐛 Ensure that `options_metavar` is passed through correctly. PR [#816](https://github.com/fastapi/typer/pull/816) by [@gar1t](https://github.com/gar1t).
* 🐛 Ensure an optional argument is shown in brackets, even when `metavar` is set. PR [#1409](https://github.com/fastapi/typer/pull/1409) by [@svlandeg](https://github.com/svlandeg).
* 🐛 Ensure that the default `rich_markup_mode` is interpreted correctly. PR [#1304](https://github.com/fastapi/typer/pull/1304) by [@svlandeg](https://github.com/svlandeg).
### Refactors
* ♻️ Refactor the handling of `shellingham`. PR [#1347](https://github.com/fastapi/typer/pull/1347) by [@nathanjmcdougall](https://github.com/nathanjmcdougall).
### Docs
* 📝 Ensure that bold letters are rendered correctly in `printing.md`. PR [#1365](https://github.com/fastapi/typer/pull/1365) by [@svlandeg](https://github.com/svlandeg).
* 🩺 Update test badge to only reflect pushes to `master`. PR [#1414](https://github.com/fastapi/typer/pull/1414) by [@svlandeg](https://github.com/svlandeg).
* 📝 Update console output on the Rich help formatting page. PR [#1430](https://github.com/fastapi/typer/pull/1430) by [@svlandeg](https://github.com/svlandeg).
* 📝 Update emoji used in Rich help formatting tutorial. PR [#1429](https://github.com/fastapi/typer/pull/1429) by [@svlandeg](https://github.com/svlandeg).
* 📝 Remove duplicate explanation how the path is resolved. PR [#956](https://github.com/fastapi/typer/pull/956) by [@dennis-rall](https://github.com/dennis-rall).
* 📝 Update docs to use `Typer()` more prominently. PR [#1418](https://github.com/fastapi/typer/pull/1418) by [@svlandeg](https://github.com/svlandeg).
* 💄 Use font 'Fira Code' to fix display of Rich panels in docs in Windows. PR [#1419](https://github.com/fastapi/typer/pull/1419) by [@tiangolo](https://github.com/tiangolo).
### Internal
* 🔨 Add `--showlocals` to `test.sh`. PR [#1169](https://github.com/fastapi/typer/pull/1169) by [@rickwporter](https://github.com/rickwporter).
* ⬆ Bump ruff from 0.14.6 to 0.14.8. PR [#1436](https://github.com/fastapi/typer/pull/1436) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1434](https://github.com/fastapi/typer/pull/1434) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ✅ Update tests to use `mod.app` . PR [#1427](https://github.com/fastapi/typer/pull/1427) by [@svlandeg](https://github.com/svlandeg).
* ⬆ Bump actions/checkout from 5 to 6. PR [#1426](https://github.com/fastapi/typer/pull/1426) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1425](https://github.com/fastapi/typer/pull/1425) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ Bump ruff from 0.14.5 to 0.14.6. PR [#1423](https://github.com/fastapi/typer/pull/1423) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump actions/checkout from 5 to 6. PR [#1417](https://github.com/fastapi/typer/pull/1417) by [@dependabot[bot]](https://github.com/apps/dependabot).
* 👷 Upgrade `latest-changes` GitHub Action and pin `actions/checkout@v5`. PR [#1424](https://github.com/fastapi/typer/pull/1424) by [@tiangolo](https://github.com/tiangolo).
* 🔧 Upgrade Material for MkDocs and remove insiders. PR [#1416](https://github.com/fastapi/typer/pull/1416) by [@tiangolo](https://github.com/tiangolo).
* ⬆ Bump mkdocs-material from 9.6.23 to 9.7.0. PR [#1404](https://github.com/fastapi/typer/pull/1404) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump mkdocs-macros-plugin from 1.4.1 to 1.5.0. PR [#1406](https://github.com/fastapi/typer/pull/1406) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump ruff from 0.14.4 to 0.14.5. PR [#1407](https://github.com/fastapi/typer/pull/1407) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1413](https://github.com/fastapi/typer/pull/1413) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ Bump ruff from 0.14.3 to 0.14.4. PR [#1402](https://github.com/fastapi/typer/pull/1402) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1403](https://github.com/fastapi/typer/pull/1403) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ Bump ruff from 0.14.2 to 0.14.3. PR [#1396](https://github.com/fastapi/typer/pull/1396) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1399](https://github.com/fastapi/typer/pull/1399) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ Bump mkdocs-material from 9.6.22 to 9.6.23. PR [#1398](https://github.com/fastapi/typer/pull/1398) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1394](https://github.com/fastapi/typer/pull/1394) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ Bump ruff from 0.14.1 to 0.14.2. PR [#1383](https://github.com/fastapi/typer/pull/1383) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump actions/upload-artifact from 4 to 5. PR [#1388](https://github.com/fastapi/typer/pull/1388) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump mkdocs-macros-plugin from 1.4.0 to 1.4.1. PR [#1389](https://github.com/fastapi/typer/pull/1389) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump actions/download-artifact from 5 to 6. PR [#1391](https://github.com/fastapi/typer/pull/1391) by [@dependabot[bot]](https://github.com/apps/dependabot).
* 🔧 Add PEP-639 license metadata. PR [#1387](https://github.com/fastapi/typer/pull/1387) by [@svlandeg](https://github.com/svlandeg).
* ⬆ Bump mypy from 1.11.2 to 1.14.1. PR [#1375](https://github.com/fastapi/typer/pull/1375) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1380](https://github.com/fastapi/typer/pull/1380) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ Bump ruff from 0.14.0 to 0.14.1. PR [#1379](https://github.com/fastapi/typer/pull/1379) by [@dependabot[bot]](https://github.com/apps/dependabot).
## 0.20.0
### Features
* ✨ Enable command suggestions on typo by default. PR [#1371](https://github.com/fastapi/typer/pull/1371) by [@savannahostrowski](https://github.com/savannahostrowski).
### Upgrades
* ⬆️ Add support for Python 3.14. PR [#1372](https://github.com/fastapi/typer/pull/1372) by [@svlandeg](https://github.com/svlandeg).
### Internal
* 👷 Add nightly workflow to run tests against CPython main branch. PR [#1374](https://github.com/fastapi/typer/pull/1374) by [@savannahostrowski](https://github.com/savannahostrowski).
* ⬆ Bump mkdocs-material from 9.6.21 to 9.6.22. PR [#1377](https://github.com/fastapi/typer/pull/1377) by [@dependabot[bot]](https://github.com/apps/dependabot).
* 🔧 Configure reminder for `waiting` label in `issue-manager`. PR [#1378](https://github.com/fastapi/typer/pull/1378) by [@YuriiMotov](https://github.com/YuriiMotov).
* ⬆ Bump ruff from 0.13.3 to 0.14.0. PR [#1368](https://github.com/fastapi/typer/pull/1368) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1376](https://github.com/fastapi/typer/pull/1376) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ Bump mkdocs-macros-plugin from 1.3.9 to 1.4.0. PR [#1354](https://github.com/fastapi/typer/pull/1354) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump mkdocs-material from 9.6.20 to 9.6.21. PR [#1360](https://github.com/fastapi/typer/pull/1360) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump mypy from 1.4.1 to 1.11.2. PR [#957](https://github.com/fastapi/typer/pull/957) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump astral-sh/setup-uv from 6 to 7. PR [#1369](https://github.com/fastapi/typer/pull/1369) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump ruff from 0.13.2 to 0.13.3. PR [#1366](https://github.com/fastapi/typer/pull/1366) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1367](https://github.com/fastapi/typer/pull/1367) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ Bump tiangolo/issue-manager from 0.5.1 to 0.6.0. PR [#1361](https://github.com/fastapi/typer/pull/1361) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump ruff from 0.13.1 to 0.13.2. PR [#1357](https://github.com/fastapi/typer/pull/1357) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1358](https://github.com/fastapi/typer/pull/1358) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* 👷 Update docs previews comment, single comment, add failure status. PR [#1359](https://github.com/fastapi/typer/pull/1359) by [@tiangolo](https://github.com/tiangolo).
## 0.19.2
### Fixes
* 🐛 Fix list convertor with an empty list default factory . PR [#1350](https://github.com/fastapi/typer/pull/1350) by [@svlandeg](https://github.com/svlandeg).
### Refactors
* 🔥 Drop support for Python 3.7. PR [#830](https://github.com/fastapi/typer/pull/830) by [@kinuax](https://github.com/kinuax).
### Internal
* ⬆ Bump ruff from 0.13.0 to 0.13.1. PR [#1339](https://github.com/fastapi/typer/pull/1339) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1351](https://github.com/fastapi/typer/pull/1351) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ Bump markdown-include-variants from 0.0.4 to 0.0.5. PR [#1348](https://github.com/fastapi/typer/pull/1348) by [@dependabot[bot]](https://github.com/apps/dependabot).
## 0.19.1
**Note**: this is the last version to support Python 3.7, going forward Typer will require Python 3.8+. And soon Python 3.8 will also be dropped as [Python 3.8 reached it's end of life](https://devguide.python.org/versions/) almost a year ago.
### Fixes
* 🐛 Ensure that `Optional[list]` values work correctly with callbacks. PR [#1018](https://github.com/fastapi/typer/pull/1018) by [@solesensei](https://github.com/solesensei).
## 0.19.0
### Features
* ✨ Support `typing.Literal` to define a set of predefined choices. PR [#429](https://github.com/fastapi/typer/pull/429) by [@blackary](https://github.com/blackary).
* ✨ Allow setting an environment variable to `None` in `CliRunner.invoke`. PR [#1303](https://github.com/fastapi/typer/pull/1303) by [@arjenzorgdoc](https://github.com/arjenzorgdoc).
### Refactors
* ✅ Use Ruff rules to ensure safe lazy-loading of `rich`. PR [#1297](https://github.com/fastapi/typer/pull/1297) by [@nathanjmcdougall](https://github.com/nathanjmcdougall).
* ✅ Avoid rich formatting in number test. PR [#1305](https://github.com/fastapi/typer/pull/1305) by [@svlandeg](https://github.com/svlandeg).
### Docs
* 📝 Clarify single-command vs multi-command behaviour in README. PR [#1268](https://github.com/fastapi/typer/pull/1268) by [@MorgenPronk](https://github.com/MorgenPronk).
## 0.18.0
### Fixes
* 👽️ Ensure compatibility with Click 8.3.0 by restoring the original `value_is_missing` function. PR [#1333](https://github.com/fastapi/typer/pull/1333) by [@svlandeg](https://github.com/svlandeg).
### Upgrades
* 📌 Remove pin for Click < 8.3.0 now that there's a fix for the changes. PR [#1346](https://github.com/fastapi/typer/pull/1346) by [@tiangolo](https://github.com/tiangolo).
## 0.17.5
### Fixes
* ⬇️ Restrict Click to below 8.3.0 to handle changes in the new version. PR [#1336](https://github.com/fastapi/typer/pull/1336) by [@svlandeg](https://github.com/svlandeg).
### Internal
* ⬆ Bump mkdocs-material from 9.6.14 to 9.6.20. PR [#1308](https://github.com/fastapi/typer/pull/1308) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump mkdocs-material from 9.5.50 to 9.6.14. PR [#1223](https://github.com/fastapi/typer/pull/1223) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump actions/download-artifact from 4 to 5. PR [#1269](https://github.com/fastapi/typer/pull/1269) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump ruff from 0.12.12 to 0.13.0. PR [#1302](https://github.com/fastapi/typer/pull/1302) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1307](https://github.com/fastapi/typer/pull/1307) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ Update pytest-cov requirement from <7.0.0,>=2.10.0 to >=2.10.0,<8.0.0. PR [#1301](https://github.com/fastapi/typer/pull/1301) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump actions/setup-python from 5 to 6. PR [#1291](https://github.com/fastapi/typer/pull/1291) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump ruff from 0.12.11 to 0.12.12. PR [#1295](https://github.com/fastapi/typer/pull/1295) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1300](https://github.com/fastapi/typer/pull/1300) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ Bump actions/labeler from 5 to 6. PR [#1296](https://github.com/fastapi/typer/pull/1296) by [@dependabot[bot]](https://github.com/apps/dependabot).
## 0.17.4
### Fixes
* 🐛 Make sure `rich.markup` is imported when rendering help text. PR [#1290](https://github.com/fastapi/typer/pull/1290) by [@g-arjones](https://github.com/g-arjones).
### Internal
* ⬆ Bump pypa/gh-action-pypi-publish from 1.12.4 to 1.13.0. PR [#1292](https://github.com/fastapi/typer/pull/1292) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump ruff from 0.12.10 to 0.12.11. PR [#1283](https://github.com/fastapi/typer/pull/1283) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1288](https://github.com/fastapi/typer/pull/1288) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* 👷 Set permissions for conflict detector workflow. PR [#1287](https://github.com/fastapi/typer/pull/1287) by [@svlandeg](https://github.com/svlandeg).
* 👷 Detect and label merge conflicts on PRs automatically. PR [#1286](https://github.com/fastapi/typer/pull/1286) by [@svlandeg](https://github.com/svlandeg).
## 0.17.3
### Features
* ✨ Allow annotated parsing with a subclass of `Path`. PR [#1183](https://github.com/fastapi/typer/pull/1183) by [@emfdavid](https://github.com/emfdavid).
## 0.17.2
### Fixes
* 🐛 Avoid printing `default: None` in the help section when using Rich. PR [#1120](https://github.com/fastapi/typer/pull/1120) by [@mattmess1221](https://github.com/mattmess1221).
## 0.17.1
### Fixes
* 🐛 Fix markdown formatting in `--help` output. PR [#815](https://github.com/fastapi/typer/pull/815) by [@gar1t](https://github.com/gar1t).
## 0.17.0
### Features
* ⚡️ Lazy-load `rich_utils` to reduce startup time. PR [#1128](https://github.com/fastapi/typer/pull/1128) by [@oefe](https://github.com/oefe).
### Internal
* ⬆ Bump ruff from 0.12.9 to 0.12.10. PR [#1280](https://github.com/fastapi/typer/pull/1280) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1281](https://github.com/fastapi/typer/pull/1281) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ Update pytest-sugar requirement from <1.1.0,>=0.9.4 to >=0.9.4,<1.2.0. PR [#1279](https://github.com/fastapi/typer/pull/1279) by [@dependabot[bot]](https://github.com/apps/dependabot).
## 0.16.1
### Fixes
* 🐛 Avoid printing additional output with `no_args_is_help=True` and Click 8.2. PR [#1278](https://github.com/fastapi/typer/pull/1278) by [@svlandeg](https://github.com/svlandeg).
### Docs
* 📝 Remove duplicated line in `environment-variables.md`. PR [#1277](https://github.com/fastapi/typer/pull/1277) by [@neirzhei](https://github.com/neirzhei).
* 📝 Fix reference to `count` parameter in the documentation. PR [#1201](https://github.com/fastapi/typer/pull/1201) by [@PokkaKiyo](https://github.com/PokkaKiyo).
### Internal
* ⬆ Bump ruff from 0.11.13 to 0.12.9. PR [#1276](https://github.com/fastapi/typer/pull/1276) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1247](https://github.com/fastapi/typer/pull/1247) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ Bump actions/checkout from 4 to 5. PR [#1271](https://github.com/fastapi/typer/pull/1271) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump mkdocs-macros-plugin from 1.3.7 to 1.3.9. PR [#1272](https://github.com/fastapi/typer/pull/1272) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump tiangolo/latest-changes from 0.3.2 to 0.4.0. PR [#1265](https://github.com/fastapi/typer/pull/1265) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump pillow from 11.2.1 to 11.3.0. PR [#1249](https://github.com/fastapi/typer/pull/1249) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1206](https://github.com/fastapi/typer/pull/1206) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ Bump pillow from 11.1.0 to 11.2.1. PR [#1198](https://github.com/fastapi/typer/pull/1198) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump cairosvg from 2.7.1 to 2.8.2. PR [#1226](https://github.com/fastapi/typer/pull/1226) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump ruff from 0.11.6 to 0.11.13. PR [#1241](https://github.com/fastapi/typer/pull/1241) by [@dependabot[bot]](https://github.com/apps/dependabot).
## 0.16.0
### Upgrades
* ⬆️ Add compatibility with Click 8.2. PR [#1222](https://github.com/fastapi/typer/pull/1222) by [@tiangolo](https://github.com/tiangolo).
When using the `CliRunner` with Click < 8.2, to be able to access the `stderr` output, you needed to set the `mix_stderr` parameter to `True`. Since Click 8.2 (and Typer 0.160 this release supporting it) this is no longer necessary, so this parameter has been removed.
### Refactors
* ✅ Refactor tests for compatibility with Click 8.2. PR [#1230](https://github.com/fastapi/typer/pull/1230) by [@tiangolo](https://github.com/tiangolo).
### Internal
* 🔧 Remove Google Analytics. PR [#1229](https://github.com/fastapi/typer/pull/1229) by [@tiangolo](https://github.com/tiangolo).
## 0.15.4
### Upgrades
* 📌 Pin Click to < 8.2, compatibility for Click >= 8.2 will be added in a future version. PR [#1225](https://github.com/fastapi/typer/pull/1225) by [@tiangolo](https://github.com/tiangolo).
## 0.15.3
### Fixes
* 🐛 Ensure that autocompletion works for `Path` arguments/options. PR [#1138](https://github.com/fastapi/typer/pull/1138) by [@svlandeg](https://github.com/svlandeg).
* 🐛 Fix newline after header in help text, and add more tests for the behaviour of `rich_markup_mode` . PR [#964](https://github.com/fastapi/typer/pull/964) by [@svlandeg](https://github.com/svlandeg).
### Internal
* ⬆ Bump astral-sh/setup-uv from 5 to 6. PR [#1203](https://github.com/fastapi/typer/pull/1203) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump ruff from 0.11.2 to 0.11.6. PR [#1200](https://github.com/fastapi/typer/pull/1200) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1196](https://github.com/fastapi/typer/pull/1196) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ Bump ruff from 0.11.1 to 0.11.2. PR [#1186](https://github.com/fastapi/typer/pull/1186) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1187](https://github.com/fastapi/typer/pull/1187) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ Bump ruff from 0.11.0 to 0.11.1. PR [#1185](https://github.com/fastapi/typer/pull/1185) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump ruff from 0.9.10 to 0.11.0. PR [#1180](https://github.com/fastapi/typer/pull/1180) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1181](https://github.com/fastapi/typer/pull/1181) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1176](https://github.com/fastapi/typer/pull/1176) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ Bump ruff from 0.9.9 to 0.9.10. PR [#1175](https://github.com/fastapi/typer/pull/1175) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1171](https://github.com/fastapi/typer/pull/1171) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ Bump ruff from 0.9.7 to 0.9.9. PR [#1166](https://github.com/fastapi/typer/pull/1166) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ✏️ Fix typo in test name. PR [#1165](https://github.com/fastapi/typer/pull/1165) by [@svlandeg](https://github.com/svlandeg).
## 0.15.2
### Features
* ✨ Allow custom styles for commands in help output. PR [#1103](https://github.com/fastapi/typer/pull/1103) by [@TheTechromancer](https://github.com/TheTechromancer).
* ✨ Avoid the unnecessary import of `typing_extensions` in newer Python versions. PR [#1048](https://github.com/fastapi/typer/pull/1048) by [@horta](https://github.com/horta).
### Fixes
* 🐛 Fix shell completions for the fish shell. PR [#1069](https://github.com/fastapi/typer/pull/1069) by [@goraje](https://github.com/goraje).
### Refactors
* 🚚 Rename test to corner-cases to make it more explicit. PR [#1083](https://github.com/fastapi/typer/pull/1083) by [@tiangolo](https://github.com/tiangolo).
### Docs
* ✏️ Fix small typos in the tutorial documentation. PR [#1137](https://github.com/fastapi/typer/pull/1137) by [@svlandeg](https://github.com/svlandeg).
* 📝 Update optional CLI argument section in tutorial with `Annotated`. PR [#983](https://github.com/fastapi/typer/pull/983) by [@gkeuccsr](https://github.com/gkeuccsr).
* 📝 Clarify the need for `mix_stderr` when accessing the output of `stderr` in tests. PR [#1045](https://github.com/fastapi/typer/pull/1045) by [@mrchrisadams](https://github.com/mrchrisadams).
### Internal
* 🔧 Add support for Python 3.13, tests in CI and add PyPI trove classifier. PR [#1091](https://github.com/fastapi/typer/pull/1091) by [@edgarrmondragon](https://github.com/edgarrmondragon).
* ⬆ Bump ruff from 0.9.6 to 0.9.7. PR [#1161](https://github.com/fastapi/typer/pull/1161) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1162](https://github.com/fastapi/typer/pull/1162) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ Bump ruff from 0.9.5 to 0.9.6. PR [#1153](https://github.com/fastapi/typer/pull/1153) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1151](https://github.com/fastapi/typer/pull/1151) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ Bump ruff from 0.9.4 to 0.9.5. PR [#1146](https://github.com/fastapi/typer/pull/1146) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1142](https://github.com/fastapi/typer/pull/1142) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ Bump ruff from 0.9.3 to 0.9.4. PR [#1139](https://github.com/fastapi/typer/pull/1139) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1135](https://github.com/fastapi/typer/pull/1135) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ Bump ruff from 0.9.1 to 0.9.3. PR [#1136](https://github.com/fastapi/typer/pull/1136) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1130](https://github.com/fastapi/typer/pull/1130) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ Bump ruff from 0.8.6 to 0.9.1. PR [#1118](https://github.com/fastapi/typer/pull/1118) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump pypa/gh-action-pypi-publish from 1.12.3 to 1.12.4. PR [#1132](https://github.com/fastapi/typer/pull/1132) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump mkdocs-material from 9.5.49 to 9.5.50. PR [#1129](https://github.com/fastapi/typer/pull/1129) by [@dependabot[bot]](https://github.com/apps/dependabot).
* 💚 Fix test matrix for Python 3.7. PR [#1116](https://github.com/fastapi/typer/pull/1116) by [@svlandeg](https://github.com/svlandeg).
* ⬆ Bump ruff from 0.8.4 to 0.8.6. PR [#1107](https://github.com/fastapi/typer/pull/1107) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1109](https://github.com/fastapi/typer/pull/1109) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ Bump pillow from 11.0.0 to 11.1.0. PR [#1104](https://github.com/fastapi/typer/pull/1104) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1102](https://github.com/fastapi/typer/pull/1102) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ Bump ruff from 0.8.3 to 0.8.4. PR [#1097](https://github.com/fastapi/typer/pull/1097) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump astral-sh/setup-uv from 4 to 5. PR [#1098](https://github.com/fastapi/typer/pull/1098) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump markdown-include-variants from 0.0.3 to 0.0.4. PR [#1100](https://github.com/fastapi/typer/pull/1100) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump ruff from 0.8.2 to 0.8.3. PR [#1090](https://github.com/fastapi/typer/pull/1090) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1093](https://github.com/fastapi/typer/pull/1093) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ Bump mkdocs-material from 9.5.48 to 9.5.49. PR [#1092](https://github.com/fastapi/typer/pull/1092) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump pypa/gh-action-pypi-publish from 1.12.2 to 1.12.3. PR [#1088](https://github.com/fastapi/typer/pull/1088) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1087](https://github.com/fastapi/typer/pull/1087) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ Bump ruff from 0.8.1 to 0.8.2. PR [#1084](https://github.com/fastapi/typer/pull/1084) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump mkdocs-material from 9.5.47 to 9.5.48. PR [#1086](https://github.com/fastapi/typer/pull/1086) by [@dependabot[bot]](https://github.com/apps/dependabot).
## 0.15.1
### Features
* 🗑️ Deprecate `shell_complete` and continue to use `autocompletion` for CLI parameters. PR [#974](https://github.com/fastapi/typer/pull/974) by [@svlandeg](https://github.com/svlandeg).
### Docs
* ✏️ Fix a few typos in the source and documentation. PR [#1028](https://github.com/fastapi/typer/pull/1028) by [@kkirsche](https://github.com/kkirsche).
* 📝 Fix minor inconsistencies and typos in tutorial. PR [#1067](https://github.com/fastapi/typer/pull/1067) by [@tvoirand](https://github.com/tvoirand).
* ✏️ Fix a few small typos in the documentation. PR [#1077](https://github.com/fastapi/typer/pull/1077) by [@svlandeg](https://github.com/svlandeg).
### Internal
* 🔧 Update build-docs filter patterns. PR [#1080](https://github.com/fastapi/typer/pull/1080) by [@tiangolo](https://github.com/tiangolo).
* 🔨 Update deploy docs preview script. PR [#1079](https://github.com/fastapi/typer/pull/1079) by [@tiangolo](https://github.com/tiangolo).
* 🔧 Update members. PR [#1078](https://github.com/fastapi/typer/pull/1078) by [@tiangolo](https://github.com/tiangolo).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1071](https://github.com/fastapi/typer/pull/1071) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ Update httpx requirement from <0.28.0,>=0.27.0 to >=0.27.0,<0.29.0. PR [#1065](https://github.com/fastapi/typer/pull/1065) by [@dependabot[bot]](https://github.com/apps/dependabot).
## 0.15.0
### Features
* ✨ Add support for extending typer apps without passing a name, add commands to the top level. PR [#1037](https://github.com/fastapi/typer/pull/1037) by [@patrick91](https://github.com/patrick91).
* New docs: [One File Per Command](https://typer.tiangolo.com/tutorial/one-file-per-command/).
### Internal
* ⬆ Bump mkdocs-material from 9.5.46 to 9.5.47. PR [#1070](https://github.com/fastapi/typer/pull/1070) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump ruff from 0.8.0 to 0.8.1. PR [#1066](https://github.com/fastapi/typer/pull/1066) by [@dependabot[bot]](https://github.com/apps/dependabot).
## 0.14.0
### Breaking Changes
* 🔥 Remove auto naming of groups added via `add_typer` based on the group's callback function name. PR [#1052](https://github.com/fastapi/typer/pull/1052) by [@patrick91](https://github.com/patrick91).
Before, it was supported to infer the name of a command group from the callback function name in the sub-app, so, in this code:
```python
import typer
app = typer.Typer()
users_app = typer.Typer()
app.add_typer(users_app)
@users_app.callback()
def users(): # <-- This was the inferred command group name
"""
Manage users in the app.
"""
@users_app.command()
def create(name: str):
print(f"Creating user: {name}")
```
...the command group would be named `users`, based on the name of the function `def users()`.
Now you need to set it explicitly:
```python
import typer
app = typer.Typer()
users_app = typer.Typer()
app.add_typer(users_app, name="users") # <-- Explicitly set the command group name
@users_app.callback()
def users():
"""
Manage users in the app.
"""
@users_app.command()
def create(name: str):
print(f"Creating user: {name}")
```
Updated docs [SubCommand Name and Help](https://typer.tiangolo.com/tutorial/subcommands/name-and-help/).
**Note**: this change will enable important features in the next release. 🤩
### Internal
* ⬆ Bump pypa/gh-action-pypi-publish from 1.10.3 to 1.12.2. PR [#1043](https://github.com/fastapi/typer/pull/1043) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump mkdocs-material from 9.5.44 to 9.5.46. PR [#1062](https://github.com/fastapi/typer/pull/1062) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump ruff from 0.7.4 to 0.8.0. PR [#1059](https://github.com/fastapi/typer/pull/1059) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump astral-sh/setup-uv from 3 to 4. PR [#1061](https://github.com/fastapi/typer/pull/1061) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1053](https://github.com/fastapi/typer/pull/1053) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
## 0.13.1
### Features
* ✨ Remove Rich tags when showing completion text. PR [#877](https://github.com/fastapi/typer/pull/877) by [@svlandeg](https://github.com/svlandeg).
* ✨ Render Rich markup as HTML in Markdown docs. PR [#847](https://github.com/fastapi/typer/pull/847) by [@svlandeg](https://github.com/svlandeg).
* ✨ Support cp850 encoding for auto-completion in PowerShell. PR [#808](https://github.com/fastapi/typer/pull/808) by [@svlandeg](https://github.com/svlandeg).
* ✨ Allow gettext translation of help message. PR [#886](https://github.com/fastapi/typer/pull/886) by [@svlandeg](https://github.com/svlandeg).
### Refactors
* 🐛 Fix printing HTML from Rich output. PR [#1055](https://github.com/fastapi/typer/pull/1055) by [@tiangolo](https://github.com/tiangolo).
### Docs
* 📝 Update markdown includes to use the new simpler format. PR [#1054](https://github.com/fastapi/typer/pull/1054) by [@tiangolo](https://github.com/tiangolo).
### Internal
* ⬆ Bump ruff from 0.7.3 to 0.7.4. PR [#1051](https://github.com/fastapi/typer/pull/1051) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1047](https://github.com/fastapi/typer/pull/1047) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ Bump ruff from 0.7.2 to 0.7.3. PR [#1046](https://github.com/fastapi/typer/pull/1046) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump tiangolo/latest-changes from 0.3.1 to 0.3.2. PR [#1044](https://github.com/fastapi/typer/pull/1044) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Update pytest-cov requirement from <6.0.0,>=2.10.0 to >=2.10.0,<7.0.0. PR [#1033](https://github.com/fastapi/typer/pull/1033) by [@dependabot[bot]](https://github.com/apps/dependabot).
## 0.13.0
### Features
* ✨ Handle `KeyboardInterrupt` separately from other exceptions. PR [#1039](https://github.com/fastapi/typer/pull/1039) by [@patrick91](https://github.com/patrick91).
* ✨ Update `launch` to not print anything when opening urls. PR [#1035](https://github.com/fastapi/typer/pull/1035) by [@patrick91](https://github.com/patrick91).
* ✨ Show help items in order of definition. PR [#944](https://github.com/fastapi/typer/pull/944) by [@svlandeg](https://github.com/svlandeg).
### Fixes
* 🐛 Fix equality check for custom classes. PR [#979](https://github.com/fastapi/typer/pull/979) by [@AryazE](https://github.com/AryazE).
* 🐛 Allow colon in zsh autocomplete values and descriptions. PR [#988](https://github.com/fastapi/typer/pull/988) by [@snapbug](https://github.com/snapbug).
### Refactors
* 🗑️ Deprecate support for `is_flag` and `flag_value` parameters. PR [#987](https://github.com/fastapi/typer/pull/987) by [@svlandeg](https://github.com/svlandeg).
* 🔥 Remove unused functionality from `_typing.py` file. PR [#805](https://github.com/fastapi/typer/pull/805) by [@ivantodorovich](https://github.com/ivantodorovich).
* ✏️ Fix typo in function name `_make_rich_text`. PR [#959](https://github.com/fastapi/typer/pull/959) by [@svlandeg](https://github.com/svlandeg).
### Internal
* ✅ Only run completion installation tests when the env var `_TYPER_RUN_INSTALL_COMPLETION_TESTS` is set. PR [#995](https://github.com/fastapi/typer/pull/995) by [@svlandeg](https://github.com/svlandeg).
* 📝 Update the docstring of the `_make_rich_text` method. PR [#972](https://github.com/fastapi/typer/pull/972) by [@svlandeg](https://github.com/svlandeg).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1040](https://github.com/fastapi/typer/pull/1040) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ Bump mkdocs-material from 9.5.42 to 9.5.44. PR [#1042](https://github.com/fastapi/typer/pull/1042) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump ruff from 0.7.1 to 0.7.2. PR [#1038](https://github.com/fastapi/typer/pull/1038) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump mkdocs-macros-plugin from 1.3.6 to 1.3.7. PR [#1031](https://github.com/fastapi/typer/pull/1031) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1032](https://github.com/fastapi/typer/pull/1032) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ Bump ruff from 0.7.0 to 0.7.1. PR [#1029](https://github.com/fastapi/typer/pull/1029) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump pillow from 10.4.0 to 11.0.0. PR [#1023](https://github.com/fastapi/typer/pull/1023) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump mkdocs-material from 9.5.35 to 9.5.42. PR [#1027](https://github.com/fastapi/typer/pull/1027) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump ruff from 0.6.5 to 0.7.0. PR [#1026](https://github.com/fastapi/typer/pull/1026) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump mkdocs-macros-plugin from 1.2.0 to 1.3.6. PR [#1025](https://github.com/fastapi/typer/pull/1025) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Update pre-commit requirement from <4.0.0,>=2.17.0 to >=2.17.0,<5.0.0. PR [#1012](https://github.com/fastapi/typer/pull/1012) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump pypa/gh-action-pypi-publish from 1.10.1 to 1.10.3. PR [#1009](https://github.com/fastapi/typer/pull/1009) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1001](https://github.com/fastapi/typer/pull/1001) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* 👷 Update Deploy docs CI to use uv. PR [#1021](https://github.com/fastapi/typer/pull/1021) by [@tiangolo](https://github.com/tiangolo).
* 👷 Fix smokeshow, checkout files on CI. PR [#1020](https://github.com/fastapi/typer/pull/1020) by [@tiangolo](https://github.com/tiangolo).
* 👷 Use uv in CI. PR [#1019](https://github.com/fastapi/typer/pull/1019) by [@tiangolo](https://github.com/tiangolo).
* 👷 Update `labeler.yml`. PR [#1014](https://github.com/fastapi/typer/pull/1014) by [@tiangolo](https://github.com/tiangolo).
* 👷 Update worfkow deploy-docs-notify URL. PR [#1011](https://github.com/fastapi/typer/pull/1011) by [@tiangolo](https://github.com/tiangolo).
* 👷 Upgrade Cloudflare GitHub Action. PR [#1010](https://github.com/fastapi/typer/pull/1010) by [@tiangolo](https://github.com/tiangolo).
* ⬆ Bump mkdocs-macros-plugin from 1.0.5 to 1.2.0. PR [#992](https://github.com/fastapi/typer/pull/992) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump ruff from 0.6.4 to 0.6.5. PR [#991](https://github.com/fastapi/typer/pull/991) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump mkdocs-material from 9.5.34 to 9.5.35. PR [#996](https://github.com/fastapi/typer/pull/996) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#993](https://github.com/fastapi/typer/pull/993) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#982](https://github.com/fastapi/typer/pull/982) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ Bump tiangolo/issue-manager from 0.5.0 to 0.5.1. PR [#980](https://github.com/fastapi/typer/pull/980) by [@dependabot[bot]](https://github.com/apps/dependabot).
* 👷 Update `issue-manager.yml`. PR [#978](https://github.com/fastapi/typer/pull/978) by [@tiangolo](https://github.com/tiangolo).
* ⬆ Bump ruff from 0.6.3 to 0.6.4. PR [#975](https://github.com/fastapi/typer/pull/975) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump mkdocs-material from 9.5.33 to 9.5.34. PR [#963](https://github.com/fastapi/typer/pull/963) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump pypa/gh-action-pypi-publish from 1.9.0 to 1.10.1. PR [#973](https://github.com/fastapi/typer/pull/973) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#966](https://github.com/fastapi/typer/pull/966) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* 💚 Set `include-hidden-files` to `True` when using the `upload-artifact` GH action. PR [#967](https://github.com/fastapi/typer/pull/967) by [@svlandeg](https://github.com/svlandeg).
* ⬆ Bump ruff from 0.6.1 to 0.6.3. PR [#961](https://github.com/fastapi/typer/pull/961) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#689](https://github.com/fastapi/typer/pull/689) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ Bump ruff from 0.2.0 to 0.6.1. PR [#938](https://github.com/fastapi/typer/pull/938) by [@dependabot[bot]](https://github.com/apps/dependabot).
* 👷 Update `latest-changes` GitHub Action. PR [#955](https://github.com/fastapi/typer/pull/955) by [@tiangolo](https://github.com/tiangolo).
## 0.12.5
### Features
* 💄 Unify the width of the Rich console for help and errors. PR [#788](https://github.com/fastapi/typer/pull/788) by [@racinmat](https://github.com/racinmat).
* 🚸 Improve assertion error message if a group is not a valid subclass. PR [#425](https://github.com/fastapi/typer/pull/425) by [@chrisburr](https://github.com/chrisburr).
### Fixes
* 🐛 Ensure `rich_markup_mode=None` disables Rich formatting. PR [#859](https://github.com/fastapi/typer/pull/859) by [@svlandeg](https://github.com/svlandeg).
* 🐛 Fix sourcing of completion path for Git Bash. PR [#801](https://github.com/fastapi/typer/pull/801) by [@svlandeg](https://github.com/svlandeg).
* 🐛 Fix PowerShell completion with incomplete word. PR [#360](https://github.com/fastapi/typer/pull/360) by [@patricksurry](https://github.com/patricksurry).
### Refactors
* 🔥 Remove Python 3.6 specific code paths. PR [#850](https://github.com/fastapi/typer/pull/850) by [@svlandeg](https://github.com/svlandeg).
* 🔥 Clean up redundant code. PR [#858](https://github.com/fastapi/typer/pull/858) by [@svlandeg](https://github.com/svlandeg).
### Docs
* ♻️ Use F-strings in Click examples in docs. PR [#891](https://github.com/fastapi/typer/pull/891) by [@svlandeg](https://github.com/svlandeg).
* 📝Add missing `main.py` in tutorial on CLI option names. PR [#868](https://github.com/fastapi/typer/pull/868) by [@fsramalho](https://github.com/fsramalho).
* 📝 Fix broken link. PR [#835](https://github.com/fastapi/typer/pull/835) by [@OhioDschungel6](https://github.com/OhioDschungel6).
* 📝 Update package docs with the latest versions of Typer and Poetry. PR [#781](https://github.com/fastapi/typer/pull/781) by [@kinuax](https://github.com/kinuax).
* 📝 Update the Progress Bar tutorial with correct output. PR [#199](https://github.com/fastapi/typer/pull/199) by [@n1ckdm](https://github.com/n1ckdm).
* 📝 Add docs and scripts to test completion in different shells. PR [#953](https://github.com/fastapi/typer/pull/953) by [@tiangolo](https://github.com/tiangolo).
* ✏️ Fix a typo in `docs/virtual-environments.md`. PR [#952](https://github.com/fastapi/typer/pull/952) by [@tiangolo](https://github.com/tiangolo).
* ✏️ Fix typo in `docs/contributing.md`. PR [#947](https://github.com/fastapi/typer/pull/947) by [@tiangolo](https://github.com/tiangolo).
* 📝 Add docs for virtual environments, environment variables, and update contributing. PR [#946](https://github.com/fastapi/typer/pull/946) by [@tiangolo](https://github.com/tiangolo).
### Internal
* 🔨 Pre-install dependencies in Docker so that testing in Docker is faster. PR [#954](https://github.com/fastapi/typer/pull/954) by [@tiangolo](https://github.com/tiangolo).
* ✅ Add `needs_bash` test fixture. PR [#888](https://github.com/fastapi/typer/pull/888) by [@svlandeg](https://github.com/svlandeg).
* ⬆ Bump mkdocs-material from 9.5.18 to 9.5.33. PR [#945](https://github.com/fastapi/typer/pull/945) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump pillow from 10.3.0 to 10.4.0. PR [#939](https://github.com/fastapi/typer/pull/939) by [@dependabot[bot]](https://github.com/apps/dependabot).
* 👷 Fix issue-manager. PR [#948](https://github.com/fastapi/typer/pull/948) by [@tiangolo](https://github.com/tiangolo).
* 🙈 Remove extra line in .gitignore. PR [#936](https://github.com/fastapi/typer/pull/936) by [@tiangolo](https://github.com/tiangolo).
* ⬆ Update pytest-cov requirement from <5.0.0,>=2.10.0 to >=2.10.0,<6.0.0. PR [#844](https://github.com/fastapi/typer/pull/844) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump pypa/gh-action-pypi-publish from 1.8.11 to 1.9.0. PR [#865](https://github.com/fastapi/typer/pull/865) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Update pytest requirement from <8.0.0,>=4.4.0 to >=4.4.0,<9.0.0. PR [#915](https://github.com/fastapi/typer/pull/915) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Update pytest-sugar requirement from <0.10.0,>=0.9.4 to >=0.9.4,<1.1.0. PR [#841](https://github.com/fastapi/typer/pull/841) by [@dependabot[bot]](https://github.com/apps/dependabot).
## 0.12.4
### Features
* ✨ Add support for Python 3.12, tests in CI and official marker. PR [#807](https://github.com/tiangolo/typer/pull/807) by [@ivantodorovich](https://github.com/ivantodorovich).
### Fixes
* 🐛 Fix support for `UnionType` (e.g. `str | None`) with Python 3.11. PR [#548](https://github.com/fastapi/typer/pull/548) by [@jonaslb](https://github.com/jonaslb).
* 🐛 Fix `zsh` autocompletion installation. PR [#237](https://github.com/fastapi/typer/pull/237) by [@alexjurkiewicz](https://github.com/alexjurkiewicz).
* 🐛 Fix usage of `Annotated` with future annotations in Python 3.7+. PR [#814](https://github.com/fastapi/typer/pull/814) by [@ivantodorovich](https://github.com/ivantodorovich).
* 🐛 Fix `shell_complete` not working for Arguments. PR [#737](https://github.com/fastapi/typer/pull/737) by [@bckohan](https://github.com/bckohan).
### Docs
* 📝 Update docs links, from tiangolo to new fastapi org. PR [#919](https://github.com/fastapi/typer/pull/919) by [@tiangolo](https://github.com/tiangolo).
* 📝 Add docs for team and repo management. PR [#917](https://github.com/tiangolo/typer/pull/917) by [@tiangolo](https://github.com/tiangolo).
### Internal
* 🔧 Add URLs to `pyproject.toml`, show up in PyPI. PR [#931](https://github.com/fastapi/typer/pull/931) by [@tiangolo](https://github.com/tiangolo).
* 👷 Do not sync labels as it overrides manually added labels. PR [#930](https://github.com/fastapi/typer/pull/930) by [@tiangolo](https://github.com/tiangolo).
* 👷 Update labeler GitHub Action to add only one label. PR [#927](https://github.com/fastapi/typer/pull/927) by [@tiangolo](https://github.com/tiangolo).
* 👷 Update labeler GitHub Actions permissions and dependencies. PR [#926](https://github.com/fastapi/typer/pull/926) by [@tiangolo](https://github.com/tiangolo).
* 👷 Add GitHub Action label-checker. PR [#925](https://github.com/fastapi/typer/pull/925) by [@tiangolo](https://github.com/tiangolo).
* 👷 Add GitHub Action labeler. PR [#924](https://github.com/fastapi/typer/pull/924) by [@tiangolo](https://github.com/tiangolo).
* 👷 Add GitHub Action add-to-project. PR [#922](https://github.com/fastapi/typer/pull/922) by [@tiangolo](https://github.com/tiangolo).
* 🔨 Update docs.py script to enable dirty reload conditionally. PR [#918](https://github.com/tiangolo/typer/pull/918) by [@tiangolo](https://github.com/tiangolo).
* 🔧 Update MkDocs previews. PR [#916](https://github.com/tiangolo/typer/pull/916) by [@tiangolo](https://github.com/tiangolo).
* 👷 Upgrade build docs configs. PR [#914](https://github.com/tiangolo/typer/pull/914) by [@tiangolo](https://github.com/tiangolo).
* 🔧 Update MkDocs to have titles in Markdown files instead of config. PR [#913](https://github.com/tiangolo/typer/pull/913) by [@tiangolo](https://github.com/tiangolo).
* 👷 Add alls-green for test-redistribute. PR [#911](https://github.com/tiangolo/typer/pull/911) by [@tiangolo](https://github.com/tiangolo).
* 👷 Update docs-previews to handle no docs changes. PR [#912](https://github.com/tiangolo/typer/pull/912) by [@tiangolo](https://github.com/tiangolo).
* 👷🏻 Show docs deployment status and preview URLs in comment. PR [#910](https://github.com/tiangolo/typer/pull/910) by [@tiangolo](https://github.com/tiangolo).
* 🔧 Enable auto dark mode from system. PR [#908](https://github.com/tiangolo/typer/pull/908) by [@tiangolo](https://github.com/tiangolo).
* 💄 Add dark mode logo. PR [#907](https://github.com/tiangolo/typer/pull/907) by [@tiangolo](https://github.com/tiangolo).
* 🔧 Update tabs and admonitions with new syntax and new MkDocs features. PR [#906](https://github.com/tiangolo/typer/pull/906) by [@tiangolo](https://github.com/tiangolo).
* 🔧 Enable MkDocs Material features. PR [#905](https://github.com/tiangolo/typer/pull/905) by [@tiangolo](https://github.com/tiangolo).
* 🔧 Enable dark mode for docs. PR [#904](https://github.com/tiangolo/typer/pull/904) by [@tiangolo](https://github.com/tiangolo).
* ➖ Do not install jieba for MkDocs Material as there are no chinese translations. PR [#903](https://github.com/tiangolo/typer/pull/903) by [@tiangolo](https://github.com/tiangolo).
* 🙈 Add MkDocs Material cache to gitignore. PR [#902](https://github.com/tiangolo/typer/pull/902) by [@tiangolo](https://github.com/tiangolo).
* 🔨 Update lint script. PR [#901](https://github.com/tiangolo/typer/pull/901) by [@tiangolo](https://github.com/tiangolo).
* 🔧 Update MkDocs configs and docs build setup. PR [#900](https://github.com/tiangolo/typer/pull/900) by [@tiangolo](https://github.com/tiangolo).
* ⬆ Bump actions/cache from 3 to 4. PR [#839](https://github.com/tiangolo/typer/pull/839) by [@dependabot[bot]](https://github.com/apps/dependabot).
* 🍱 Update Typer icon and logo. PR [#899](https://github.com/tiangolo/typer/pull/899) by [@tiangolo](https://github.com/tiangolo).
* 👷 Update issue-manager.yml GitHub Action permissions. PR [#897](https://github.com/tiangolo/typer/pull/897) by [@tiangolo](https://github.com/tiangolo).
* 👷 Refactor GitHub Action to comment docs deployment URLs and update token, preparing for GitHub org. PR [#896](https://github.com/tiangolo/typer/pull/896) by [@tiangolo](https://github.com/tiangolo).
* 🔨 Update docs Termynal scripts to not include line nums for local dev. PR [#882](https://github.com/tiangolo/typer/pull/882) by [@tiangolo](https://github.com/tiangolo).
* ⬆ Bump black from 23.3.0 to 24.3.0. PR [#837](https://github.com/tiangolo/typer/pull/837) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump pillow from 10.1.0 to 10.3.0. PR [#836](https://github.com/tiangolo/typer/pull/836) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ✅ Add CI configs to run tests on Windows and MacOS. PR [#824](https://github.com/tiangolo/typer/pull/824) by [@svlandeg](https://github.com/svlandeg).
* 👷 Update GitHub Actions to upload and download artifacts. PR [#829](https://github.com/tiangolo/typer/pull/829) by [@tiangolo](https://github.com/tiangolo).
* 👷 Tweak CI for test-redistribute, add needed env vars for slim. PR [#827](https://github.com/tiangolo/typer/pull/827) by [@tiangolo](https://github.com/tiangolo).
* ✅ Generalize test suite to run on Windows. PR [#810](https://github.com/tiangolo/typer/pull/810) by [@svlandeg](https://github.com/svlandeg).
* ✅ Add `__init__.py` files to fix test suite. PR [#809](https://github.com/tiangolo/typer/pull/809) by [@svlandeg](https://github.com/svlandeg).
* 🔧 Update MkDocs Material, enable plugins. PR [#813](https://github.com/tiangolo/typer/pull/813) by [@tiangolo](https://github.com/tiangolo).
* 🔧 Tweak development scripts and configs after migration to PDM, Ruff, etc.. PR [#797](https://github.com/tiangolo/typer/pull/797) by [@tiangolo](https://github.com/tiangolo).
## 0.12.3
### Fixes
* 🐛 Fix Rich formatting with no commands. PR [#796](https://github.com/tiangolo/typer/pull/796) by [@svlandeg](https://github.com/svlandeg).
## 0.12.2
### Features
* ✨ Improve column help display, ensure commands column width is the same on all panels. PR [#567](https://github.com/tiangolo/typer/pull/567) by [@ssbarnea](https://github.com/ssbarnea).
### Fixes
* 🐛 Add support for an argument of type `Optional[Tuple]` and default value `None`. PR [#757](https://github.com/tiangolo/typer/pull/757) by [@Asthestarsfalll](https://github.com/Asthestarsfalll).
### Docs
* 🔧 Fix typo in Github template. PR [#793](https://github.com/tiangolo/typer/pull/793) by [@svlandeg](https://github.com/svlandeg).
* 📝 Fix typos in documentation. PR [#761](https://github.com/tiangolo/typer/pull/761) by [@svlandeg](https://github.com/svlandeg).
* 📝 Update console output with Click 8 messages. PR [#789](https://github.com/tiangolo/typer/pull/789) by [@svlandeg](https://github.com/svlandeg).
* 📝 Remove references to a .rst README generated by poetry new. PR [#632](https://github.com/tiangolo/typer/pull/632) by [@jonasmmiguel](https://github.com/jonasmmiguel).
## 0.12.1
Now you don't need to install `typer[all]`. When you install `typer` it comes with the default optional dependencies and the `typer` command.
If you don't want the extra optional dependencies (`rich` and `shellingham`), you can install `typer-slim` instead.
You can also install `typer-slim[standard]`, which includes the default optional dependencies, but not the `typer` command.
Now the package `typer-cli` doesn't add anything on top of what `typer` has, it only depends on `typer`, and is there only for backwards compatibility, so that projects that depend on `typer-cli` can get the latest features of the `typer` command while they upgrade their dependencies to require `typer` directly.
### Features
* ✨ Add support for `typer ./someprogram.py utils docs --title`. PR [#782](https://github.com/tiangolo/typer/pull/782) by [@tiangolo](https://github.com/tiangolo).
### Fixes
* 🐛 Fix broken installation when upgrading from `typer <0.12.0` to `typer >=0.12.0`, make `typer` independent of `typer-slim`, include `typer` command in `typer` package. PR [#791](https://github.com/tiangolo/typer/pull/791) by [@tiangolo](https://github.com/tiangolo).
This fixes a problem that would break the `typer` installation directory when upgrading from `typer <0.12.0` to `typer >=0.12.0`, see issue [#790](https://github.com/tiangolo/typer/issues/790).
By installing the latest version (`0.12.1`) it fixes it, for any previous version, even if the installation directory was already broken by the previous upgrade.
### Internal
* 👷 Add cron to run test once a week on monday. PR [#783](https://github.com/tiangolo/typer/pull/783) by [@estebanx64](https://github.com/estebanx64).
## 0.12.0
In version `0.12.0`, the `typer` package depends on `typer-slim[standard]` which includes the default dependencies (instead of `typer[all]`) and `typer-cli` (that provides the `typer` command).
If you don't want the extra optional dependencies (`rich` and `shellingham`), you can install `typer-slim` instead.
You can also install `typer-slim[standard]`, which includes the default optional dependencies, but not the `typer` command.
In version `0.12.0` the `typer-cli` package only provides the `typer` command, but the code is still in the main code, so even without installing `typer-cli`, it can be called with `python -m typer`.
This approach of having `typer` depend on `typer-slim[standard]` instead of including the whole code and dependencies itself caused an issue when upgrading from `typer <0.12.0` to `typer >=0.12.0`, see issue [#790](https://github.com/tiangolo/typer/issues/790). This is fixed in version `0.12.1`.
### Features
* ✨ Add `typer-slim` package without extras, make `typer` include `typer-slim[default]` and integrate Typer CLI (`typer` command) into Typer. PR [#780](https://github.com/tiangolo/typer/pull/780) by [@tiangolo](https://github.com/tiangolo).
### Internal
* 🔧 Temporarily disable social plugin while a MkDocs issue is handled. PR [#779](https://github.com/tiangolo/typer/pull/779) by [@tiangolo](https://github.com/tiangolo).
* 👷 Fix install MkDocs Insiders only when available. PR [#778](https://github.com/tiangolo/typer/pull/778) by [@tiangolo](https://github.com/tiangolo).
## 0.11.1
### Fixes
* 🔧 Explicitly include testing files in sdist for redistributors (e.g. OpenSUSE) and add CI to test redistribution. PR [#773](https://github.com/tiangolo/typer/pull/773) by [@tiangolo](https://github.com/tiangolo).
### Internal
* 👷 Do not use the cache for dependencies when publishing to PyPI. PR [#774](https://github.com/tiangolo/typer/pull/774) by [@tiangolo](https://github.com/tiangolo).
## 0.11.0
### Breaking Changes
* 🔧 Refactor package manager, move from Flit to PDM, remove private pip extras for `test`, `doc`, `dev`. PR [#764](https://github.com/tiangolo/typer/pull/764) by [@tiangolo](https://github.com/tiangolo).
* 🔥 Remove support for Click 7, require Click 8+. PR [#760](https://github.com/tiangolo/typer/pull/760) by [@tiangolo](https://github.com/tiangolo).
* 🔥 Remove support for Python 3.6. PR [#758](https://github.com/tiangolo/typer/pull/758) by [@tiangolo](https://github.com/tiangolo).
### Refactors
* 🔧 Migrate from Black, isort, flake8, autoflake, pyupgrade to Ruff. PR [#763](https://github.com/tiangolo/typer/pull/763) by [@tiangolo](https://github.com/tiangolo).
### Internal
* ⬆️ Upgrade coverage and configs. PR [#769](https://github.com/tiangolo/typer/pull/769) by [@tiangolo](https://github.com/tiangolo).
* 🔧 Upgrade mypy and config. PR [#768](https://github.com/tiangolo/typer/pull/768) by [@tiangolo](https://github.com/tiangolo).
* 👷 Upgrade Smokeshow GitHub action. PR [#767](https://github.com/tiangolo/typer/pull/767) by [@tiangolo](https://github.com/tiangolo).
* 👷 Upgrade latest-changes GitHub Action. PR [#766](https://github.com/tiangolo/typer/pull/766) by [@tiangolo](https://github.com/tiangolo).
* 👷 Upgrade issue-manager GitHub Action. PR [#765](https://github.com/tiangolo/typer/pull/765) by [@tiangolo](https://github.com/tiangolo).
* 👷 Add alls-green to CI. PR [#759](https://github.com/tiangolo/typer/pull/759) by [@tiangolo](https://github.com/tiangolo).
## 0.10.0
### Fixes
* 🐛 Fix default value of `None` for CLI Parameters when the type is `list | None` and the default value is `None`. PR [#664](https://github.com/tiangolo/typer/pull/664) by [@theowisear](https://github.com/theowisear).
## 0.9.4
### Features
* ✨ Improve support for CLI translations using gettext. PR [#417](https://github.com/tiangolo/typer/pull/417) by [@mjodmj](https://github.com/mjodmj).
## 0.9.3
### Fixes
* 🐛 Fix evaluating stringified annotations in Python 3.10 (also `from __future__ import annotations`). PR [#721](https://github.com/tiangolo/typer/pull/721) by [@heckad](https://github.com/heckad).
## 0.9.2
### Fixes
* 🐛 Fix display of default value for Enum parameters inside of a list, include docs and tests. PR [#473](https://github.com/tiangolo/typer/pull/473) by [@asieira](https://github.com/asieira).
* 🐛 Update type annotations for `show_default` parameter and update docs for setting a "Custom default string". PR [#501](https://github.com/tiangolo/typer/pull/501) by [@plannigan](https://github.com/plannigan).
### Docs
* 📝 Add docs and test for `no_args_is_help` feature. PR [#751](https://github.com/tiangolo/typer/pull/751) by [@svlandeg](https://github.com/svlandeg).
## 0.9.1
### Fixes
* 🐛 Add missing `default_factory` in `Argument` overloads. PR [#750](https://github.com/tiangolo/typer/pull/750) by [@m9810223](https://github.com/m9810223).
* 🐛 Fix preserving case in enum values. PR [#571](https://github.com/tiangolo/typer/pull/571) by [@avaldebe](https://github.com/avaldebe).
### Docs
* 📝 Remove obsolete references to `--install-completion` for `typer.run()` scripts. PR [#595](https://github.com/tiangolo/typer/pull/595) by [@tiangolo](https://github.com/tiangolo).
* 📝 Update docs example for a Typer/Click group to make new subcommands explicit. PR [#755](https://github.com/tiangolo/typer/pull/755) by [@svlandeg](https://github.com/svlandeg).
* 📝 Update docs for building a package, file structure example. PR [#683](https://github.com/tiangolo/typer/pull/683) by [@davidbgk](https://github.com/davidbgk).
* 📝 Update link in docs to the newest stable version of click. PR [#675](https://github.com/tiangolo/typer/pull/675) by [@javier171188](https://github.com/javier171188).
* 🔧 Add `CITATION.cff` file for academic citations. PR [#681](https://github.com/tiangolo/typer/pull/681) by [@tiangolo](https://github.com/tiangolo).
* ✏ Fix typo in `docs/tutorial/exceptions.md`. PR [#702](https://github.com/tiangolo/typer/pull/702) by [@menzenski](https://github.com/menzenski).
* ✏ Fix typo in `docs/tutorial/options/name.md`. PR [#725](https://github.com/tiangolo/typer/pull/725) by [@bwagner](https://github.com/bwagner).
* ✏ Fix typo in `docs/tutorial/arguments/optional.md`. PR [#602](https://github.com/tiangolo/typer/pull/602) by [@tadasgedgaudas](https://github.com/tadasgedgaudas).
### Internal
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#606](https://github.com/tiangolo/typer/pull/606) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* 👷 Install MkDocs Material Insiders only when secrets are available, for Dependabot. PR [#685](https://github.com/tiangolo/typer/pull/685) by [@tiangolo](https://github.com/tiangolo).
* ⚒️ Update build-docs.yml, do not zip docs. PR [#645](https://github.com/tiangolo/typer/pull/645) by [@tiangolo](https://github.com/tiangolo).
* 👷 Deploy docs to Cloudflare. PR [#644](https://github.com/tiangolo/typer/pull/644) by [@tiangolo](https://github.com/tiangolo).
* 👷 Upgrade CI for docs. PR [#642](https://github.com/tiangolo/typer/pull/642) by [@tiangolo](https://github.com/tiangolo).
* 👷 Update token for latest changes. PR [#635](https://github.com/tiangolo/typer/pull/635) by [@tiangolo](https://github.com/tiangolo).
* 👷 Update CI workflow dispatch for latest changes. PR [#643](https://github.com/tiangolo/typer/pull/643) by [@tiangolo](https://github.com/tiangolo).
* 👷 Update token for Material for MkDocs Insiders. PR [#636](https://github.com/tiangolo/typer/pull/636) by [@tiangolo](https://github.com/tiangolo).
* 🐛 Fix internal type annotations and bump mypy version. PR [#638](https://github.com/tiangolo/typer/pull/638) by [@paulo-raca](https://github.com/paulo-raca).
* 💡 Add comments to document overload definitions in code. PR [#752](https://github.com/tiangolo/typer/pull/752) by [@svlandeg](https://github.com/svlandeg).
* 🔥 Remove Jina QA Bot as it has been discontinued. PR [#749](https://github.com/tiangolo/typer/pull/749) by [@tiangolo](https://github.com/tiangolo).
* 👷 Update build docs CI cache paths. PR [#707](https://github.com/tiangolo/typer/pull/707) by [@tiangolo](https://github.com/tiangolo).
* 👷 Upgrade latest-changes GitHub Action. PR [#691](https://github.com/tiangolo/typer/pull/691) by [@tiangolo](https://github.com/tiangolo).
## 0.9.0
### Features
* ✨ Add support for PEP-593 `Annotated` for specifying options and arguments. Initial PR [#584](https://github.com/tiangolo/typer/pull/584) by [@ryangalamb](https://github.com/ryangalamb).
* New docs: [Optional CLI arguments](https://typer.tiangolo.com/tutorial/arguments/optional/#an-alternative-cli-argument-declaration).
* It is no longer required to pass a default value of `...` to mark a *CLI Argument* or *CLI Option* as required.
* It is now recommended to use `Annotated` for `typer.Option()` and `typer.Argument()`.
* All the docs have been updated to recommend `Annotated`.
### Docs
* 📝 Update docs examples for custom param types using `Annotated`, fix overloads for `typer.Argument`. PR [#594](https://github.com/tiangolo/typer/pull/594) by [@tiangolo](https://github.com/tiangolo).
### Internal
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#592](https://github.com/tiangolo/typer/pull/592) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
## 0.8.0
### Features
* ✨ Add support for custom types and parsers. Initial PR [#583](https://github.com/tiangolo/typer/pull/583) by [@jpurviance](https://github.com/jpurviance). Based on original PR [#443](https://github.com/tiangolo/typer/pull/443) by [@paulo-raca](https://github.com/paulo-raca).
* New docs: [CLI Parameter Types: Custom Types](https://typer.tiangolo.com/tutorial/parameter-types/custom-types/).
### Upgrades
* ⬆ Upgrade Rich, support 13.x. PR [#524](https://github.com/tiangolo/typer/pull/524) by [@musicinmybrain](https://github.com/musicinmybrain).
### Docs
* 📝 Tweak docs, Custom Types path, main page and READAME colors, broken links. PR [#588](https://github.com/tiangolo/typer/pull/588) by [@tiangolo](https://github.com/tiangolo).
* ✏ Fix spelling (shinny -> shiny). PR [#586](https://github.com/tiangolo/typer/pull/586) by [@runofthemill](https://github.com/runofthemill).
* 📝 Update docs about helping Typer. PR [#547](https://github.com/tiangolo/typer/pull/547) by [@tiangolo](https://github.com/tiangolo).
* ✏️ Fix typo in datetime docs. PR [#495](https://github.com/tiangolo/typer/pull/495) by [@huxuan](https://github.com/huxuan).
* ✏️ Add quotes to package name that includes brackets in docs. PR [#475](https://github.com/tiangolo/typer/pull/475) by [@gjolga](https://github.com/gjolga).
### Internal
* ⬆ Bump dawidd6/action-download-artifact from 2.24.2 to 2.26.0. PR [#558](https://github.com/tiangolo/typer/pull/558) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#549](https://github.com/tiangolo/typer/pull/549) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* 🔧 Add `exclude_lines` to coverage configuration. PR [#585](https://github.com/tiangolo/typer/pull/585) by [@dmontagu](https://github.com/dmontagu).
* ⬆️ Upgrade analytics. PR [#557](https://github.com/tiangolo/typer/pull/557) by [@tiangolo](https://github.com/tiangolo).
* 🔧 Update new issue chooser to suggest GitHub Discussions. PR [#544](https://github.com/tiangolo/typer/pull/544) by [@tiangolo](https://github.com/tiangolo).
* 🔧 Add GitHub Discussion templates for questions. PR [#541](https://github.com/tiangolo/typer/pull/541) by [@tiangolo](https://github.com/tiangolo).
* 🔧 Update pre-commit, Python version, isort version. PR [#542](https://github.com/tiangolo/typer/pull/542) by [@tiangolo](https://github.com/tiangolo).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#512](https://github.com/tiangolo/typer/pull/512) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ Bump nwtgck/actions-netlify from 1.2.4 to 2.0.0. PR [#513](https://github.com/tiangolo/typer/pull/513) by [@dependabot[bot]](https://github.com/apps/dependabot).
* 👷 Refactor CI artifact upload/download for docs previews. PR [#516](https://github.com/tiangolo/typer/pull/516) by [@tiangolo](https://github.com/tiangolo).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#500](https://github.com/tiangolo/typer/pull/500) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ Bump actions/cache from 2 to 3. PR [#496](https://github.com/tiangolo/typer/pull/496) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump dawidd6/action-download-artifact from 2.24.1 to 2.24.2. PR [#494](https://github.com/tiangolo/typer/pull/494) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump dawidd6/action-download-artifact from 2.9.0 to 2.24.1. PR [#491](https://github.com/tiangolo/typer/pull/491) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump actions/setup-python from 2 to 4. PR [#492](https://github.com/tiangolo/typer/pull/492) by [@dependabot[bot]](https://github.com/apps/dependabot).
* 👷♂️ Consistently use `sys.executable` to run subprocesses, needed by OpenSUSE. PR [#408](https://github.com/tiangolo/typer/pull/408) by [@theMarix](https://github.com/theMarix).
* 👷♂️ Ensure the `PYTHONPATH` is set properly when testing the tutorial scripts. PR [#407](https://github.com/tiangolo/typer/pull/407) by [@theMarix](https://github.com/theMarix).
## 0.7.0
### Features
* ✨ Make `typer.run()` not add completion scripts by default, it only makes sense in installed apps. Also update docs for handling [autocompletion in CLI options](https://typer.tiangolo.com/tutorial/options-autocompletion/). PR [#488](https://github.com/tiangolo/typer/pull/488) by [@tiangolo](https://github.com/tiangolo).
* ✨ Add support for Python 3.11, tests in CI and official marker. PR [#487](https://github.com/tiangolo/typer/pull/487) by [@tiangolo](https://github.com/tiangolo).
* 👷 Add CI for Python 3.10. PR [#384](https://github.com/tiangolo/typer/pull/384) by [@tiangolo](https://github.com/tiangolo).
### Fixes
* 🎨 Fix type annotation of `typer.run()`. PR [#284](https://github.com/tiangolo/typer/pull/284) by [@yassu](https://github.com/yassu).
* 🎨 Fix type annotations for `get_group`. PR [#430](https://github.com/tiangolo/typer/pull/430) by [@tiangolo](https://github.com/tiangolo).
### Docs
* 📝 Add note about how subcommands with function names using underscores are converted to dashes. PR [#403](https://github.com/tiangolo/typer/pull/403) by [@targhs](https://github.com/targhs).
* 📝 Fix typo in docs at `docs/tutorial/commands/help.md`. PR [#466](https://github.com/tiangolo/typer/pull/466) by [@fepegar](https://github.com/fepegar).
* ✏ Fix link in docs to `datetime.strptime()`. PR [#464](https://github.com/tiangolo/typer/pull/464) by [@Kobu](https://github.com/Kobu).
* ✏ Update `first-steps.md`, clarify distinction between parameter and argument. PR [#176](https://github.com/tiangolo/typer/pull/176) by [@mccarthysean](https://github.com/mccarthysean).
* ✏ Fix broken plac link. PR [#275](https://github.com/tiangolo/typer/pull/275) by [@mgielda](https://github.com/mgielda).
### Internal
* ✅ Add extra tests just for coverage because monkeypatching with strange imports confuses coverage. PR [#490](https://github.com/tiangolo/typer/pull/490) by [@tiangolo](https://github.com/tiangolo).
* 🔧 Tweak pytest coverage. PR [#485](https://github.com/tiangolo/typer/pull/485) by [@tiangolo](https://github.com/tiangolo).
* ➕ Bring back pytest-cov because coverage can't detect pytest-xdist. PR [#484](https://github.com/tiangolo/typer/pull/484) by [@tiangolo](https://github.com/tiangolo).
* ⬆ Bump actions/upload-artifact from 2 to 3. PR [#477](https://github.com/tiangolo/typer/pull/477) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump actions/checkout from 2 to 3. PR [#478](https://github.com/tiangolo/typer/pull/478) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#411](https://github.com/tiangolo/typer/pull/411) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ Bump nwtgck/actions-netlify from 1.1.5 to 1.2.4. PR [#479](https://github.com/tiangolo/typer/pull/479) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump tiangolo/issue-manager from 0.2.0 to 0.4.0. PR [#481](https://github.com/tiangolo/typer/pull/481) by [@dependabot[bot]](https://github.com/apps/dependabot).
* 👷 Move from pytest-cov to coverage and Codecov to Smokeshow. PR [#483](https://github.com/tiangolo/typer/pull/483) by [@tiangolo](https://github.com/tiangolo).
* ➕ Add extra Material for MkDocs deps for docs. PR [#482](https://github.com/tiangolo/typer/pull/482) by [@tiangolo](https://github.com/tiangolo).
* 🔧 Update Dependabot config. PR [#476](https://github.com/tiangolo/typer/pull/476) by [@tiangolo](https://github.com/tiangolo).
## 0.6.1
### Fixes
* 🐛 Fix setting `FORCE_TERMINAL` with colors 2. PR [#424](https://github.com/tiangolo/typer/pull/424) by [@tiangolo](https://github.com/tiangolo).
* 🐛 Fix setting `FORCE_TERMINAL` with colors. PR [#423](https://github.com/tiangolo/typer/pull/423) by [@tiangolo](https://github.com/tiangolo).
## 0.6.0
This release adds deep integrations with [Rich](https://rich.readthedocs.io/en/stable/). ✨
`rich` is an optional dependency, you can install it directly or it will be included when you install with:
```console
$ pip install "typer[all]"
```
If Rich is available, it will be used to show the content from `--help` options, validation errors, and even errors in your app (exception tracebacks).
There are new options to group commands, *CLI arguments*, and *CLI options*, support for [Rich Console Markup](https://rich.readthedocs.io/en/stable/markup.html), and more! 🎉
### Features
* ✨ Richify, add integrations with Rich everywhere. PR [#419](https://github.com/tiangolo/typer/pull/419) by [@tiangolo](https://github.com/tiangolo).
* Recommend Rich as the main information displaying tool, new docs: [Printing and Colors](https://typer.tiangolo.com/tutorial/printing/).
* For most use cases not using Rich, use plain `print()` instead of `typer.echo()` in the docs, to simplify the concepts and avoid confusions. New docs: [Printing and Colors - typer Echo](https://typer.tiangolo.com/tutorial/printing/#typer-echo).
* Define help panels for *CLI arguments*, new docs: [CLI Arguments with Help - CLI Argument help panels](https://typer.tiangolo.com/tutorial/arguments/help/#cli-argument-help-panels).
* Define help panels for *CLI options*, new docs: [CLI Options with Help - CLI Options help panels](https://typer.tiangolo.com/tutorial/options/help/#cli-options-help-panels).
* New docs for deprecating commands: [Commands - Command Help - Deprecate a Command](https://typer.tiangolo.com/tutorial/commands/help/#deprecate-a-command).
* Support for Rich Markdown in docstrings, *CLI parameters* `help`, and `epilog` with the new parameter `typer.Typer(rich_markup_mode="markdown")`, new docs: [Commands - Command Help - Rich Markdown and Markup](https://typer.tiangolo.com/tutorial/commands/help/#rich-markdown-and-markup).
* Support for Rich Markup (different from Markdown) in docstrings, *CLI parameters* `help`, and `epilog` with the new parameter `typer.Typer(rich_markup_mode="rich")`, new docs: [Commands - Command Help - Rich Markdown and Markup](https://typer.tiangolo.com/tutorial/commands/help/#rich-markdown-and-markup).
* Define help panels for *commands*, new docs: [Commands - Command Help - Help Panels](https://typer.tiangolo.com/tutorial/commands/help/#help-panels).
* New docs for setting an `epilog`, with support for Rich Markdown and Console Markup, new docs: [Commands - Command Help - Epilog](https://typer.tiangolo.com/tutorial/commands/help/#epilog).
* ✨ Refactor and document handling pretty exceptions. PR [#422](https://github.com/tiangolo/typer/pull/422) by [@tiangolo](https://github.com/tiangolo).
* Add support for customizing pretty short errors, new docs: [Exceptions and Errors](https://typer.tiangolo.com/tutorial/exceptions/).
* ✨ Allow configuring pretty errors when creating the Typer instance. PR [#416](https://github.com/tiangolo/typer/pull/416) by [@tiangolo](https://github.com/tiangolo).
### Docs
* 📝 Add docs for using Rich with Typer. PR [#421](https://github.com/tiangolo/typer/pull/421) by [@tiangolo](https://github.com/tiangolo).
* Add new docs: [Ask with Prompt - Prompt with Rich](https://typer.tiangolo.com/tutorial/prompt/#prompt-with-rich).
* Add new docs to handle progress bars and spinners with Rich: [Progress Par](https://typer.tiangolo.com/tutorial/progressbar/).
### Internal
* ⬆️ Upgrade codecov GitHub Action. PR [#420](https://github.com/tiangolo/typer/pull/420) by [@tiangolo](https://github.com/tiangolo).
## 0.5.0
### Features
* ✨ Add pretty error tracebacks for user errors and support for Rich. PR [#412](https://github.com/tiangolo/typer/pull/412) by [@tiangolo](https://github.com/tiangolo).
### Docs
* ✏ Fix typo, "ASCII codes" to "ANSI escape sequences". PR [#308](https://github.com/tiangolo/typer/pull/308) by [@septatrix](https://github.com/septatrix).
## 0.4.2
### Fixes
* 🐛 Fix type conversion for `List` and `Tuple` and their internal types. PR [#143](https://github.com/tiangolo/typer/pull/143) by [@hellowhistler](https://github.com/hellowhistler).
* 🐛 Fix `context_settings` for a Typer app with a single command. PR [#210](https://github.com/tiangolo/typer/pull/210) by [@daddycocoaman](https://github.com/daddycocoaman).
### Docs
* 📝 Clarify testing documentation about checking `stderr`. PR [#335](https://github.com/tiangolo/typer/pull/335) by [@cgabard](https://github.com/cgabard).
* ✏ Fix typo in docs for CLI Option autocompletion. PR [#288](https://github.com/tiangolo/typer/pull/288) by [@graue70](https://github.com/graue70).
* 🎨 Fix header format for "Standard Input" in `docs/tutorial/printing.md`. PR [#386](https://github.com/tiangolo/typer/pull/386) by [@briancohan](https://github.com/briancohan).
* ✏ Fix typo in `docs/tutorial/terminating.md`. PR [#382](https://github.com/tiangolo/typer/pull/382) by [@kianmeng](https://github.com/kianmeng).
* ✏ Fix syntax typo in `docs/tutorial/package.md`. PR [#333](https://github.com/tiangolo/typer/pull/333) by [@ryanstreur](https://github.com/ryanstreur).
* ✏ Fix typo, duplicated word in `docs/tutorial/options/required.md`.. PR [#316](https://github.com/tiangolo/typer/pull/316) by [@michaelriri](https://github.com/michaelriri).
* ✏ Fix minor typo in `index.md`. PR [#274](https://github.com/tiangolo/typer/pull/274) by [@RmStorm](https://github.com/RmStorm).
* ✏ Fix double "and" typo in first-steps tutorial. PR [#225](https://github.com/tiangolo/typer/pull/225) by [@softwarebloat](https://github.com/softwarebloat).
* 🎨 Fix format in docs explaining `datetime` parameter type. PR [#220](https://github.com/tiangolo/typer/pull/220) by [@DiegoPiloni](https://github.com/DiegoPiloni).
### Internal
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#404](https://github.com/tiangolo/typer/pull/404) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* 👷 Fix Material for MkDocs install in CI. PR [#395](https://github.com/tiangolo/typer/pull/395) by [@tiangolo](https://github.com/tiangolo).
* 👷 Add pre-commit CI config. PR [#394](https://github.com/tiangolo/typer/pull/394) by [@tiangolo](https://github.com/tiangolo).
* 👷 Clear MkDocs Insiders cache. PR [#393](https://github.com/tiangolo/typer/pull/393) by [@tiangolo](https://github.com/tiangolo).
* 🔧 Add pre-commit config and formatting. PR [#392](https://github.com/tiangolo/typer/pull/392) by [@tiangolo](https://github.com/tiangolo).
* 👷 Disable installing MkDocs Insiders in forks. PR [#391](https://github.com/tiangolo/typer/pull/391) by [@tiangolo](https://github.com/tiangolo).
* ⬆️ Upgrade Codecov GitHub Action. PR [#383](https://github.com/tiangolo/typer/pull/383) by [@tiangolo](https://github.com/tiangolo).
## 0.4.1
### Fixes
* 🐛 Fix import of `get_terminal_size` for Click 8.1.0 support and upgrade Black to fix CI. PR [#380](https://github.com/tiangolo/typer/pull/380) by [@tiangolo](https://github.com/tiangolo) based on original PR [#375](https://github.com/tiangolo/typer/pull/375) by [@madkinsz](https://github.com/madkinsz).
### Internal
* 📝 Add Jina's QA Bot to the docs to help people that want to ask quick questions. PR [#368](https://github.com/tiangolo/typer/pull/368) by [@tiangolo](https://github.com/tiangolo).
* 💚 Only test on push when on master, avoid duplicate CI runs from PRs. PR [#358](https://github.com/tiangolo/typer/pull/358) by [@tiangolo](https://github.com/tiangolo).
* ✨ Add support for previewing docs in PRs from forks and enable MkDocs Insiders. PR [#357](https://github.com/tiangolo/typer/pull/357) by [@tiangolo](https://github.com/tiangolo).
* ⬆️ Upgrade MkDocs Material, MDX-Include, and MkDocs structure. PR [#356](https://github.com/tiangolo/typer/pull/356) by [@tiangolo](https://github.com/tiangolo).
* 👷 Update publish GitHub action. PR [#325](https://github.com/tiangolo/typer/pull/325) by [@tiangolo](https://github.com/tiangolo).
## 0.4.0
### Features
* ✨ Add support for Click 8 while keeping compatibility with Click 7. PR [#317](https://github.com/tiangolo/typer/pull/317) by [@tiangolo](https://github.com/tiangolo).
### Internal
* 📝 Add Security policy. PR [#324](https://github.com/tiangolo/typer/pull/324) by [@tiangolo](https://github.com/tiangolo).
* 🔧 Add updated issue templates. PR [#323](https://github.com/tiangolo/typer/pull/323) by [@tiangolo](https://github.com/tiangolo).
* 👷 Enable tests for Python 3.9. PR [#322](https://github.com/tiangolo/typer/pull/322) by [@tiangolo](https://github.com/tiangolo).
* 👷 Add GitHub Action Latest Changes. PR [#321](https://github.com/tiangolo/typer/pull/321) by [@tiangolo](https://github.com/tiangolo).
* 👷 Update docs CI name. PR [#320](https://github.com/tiangolo/typer/pull/320) by [@tiangolo](https://github.com/tiangolo).
* 🔧 Add sponsors docs and badge. PR [#319](https://github.com/tiangolo/typer/pull/319) by [@tiangolo](https://github.com/tiangolo).
## 0.3.2
### Features
* Add support for `mypy --strict`. Original PR [#147](https://github.com/tiangolo/typer/pull/147) by [@victorphoenix3](https://github.com/victorphoenix3).
### Docs
* Update docs with new `--help` showing default values. PR [#135](https://github.com/tiangolo/typer/pull/135) by [@victorphoenix3](https://github.com/victorphoenix3).
* Add `Optional` to docs for *CLI Arguments and Options* with a default of `None`. PR [#131](https://github.com/tiangolo/typer/pull/131) by [@rkbeatss](https://github.com/rkbeatss).
* Add valid date formats to docs. PR [#122](https://github.com/tiangolo/typer/pull/122) by [@IamCathal](https://github.com/IamCathal).
### Internal
* Report coverage in XML to support GitHub Actions. PR [#146](https://github.com/tiangolo/typer/pull/146).
* Update badges and remove Travis, now that GitHub Actions is the main CI. PR [#145](https://github.com/tiangolo/typer/pull/145).
## 0.3.1
* Add GitHub Actions, move from Travis. PR [#144](https://github.com/tiangolo/typer/pull/144).
* Pin dependencies. PR [#138](https://github.com/tiangolo/typer/pull/138).
* Add Dependabot. PR [#136](https://github.com/tiangolo/typer/pull/136).
* Upgrade Isort to version 5.x.x. PR [#137](https://github.com/tiangolo/typer/pull/137).
## 0.3.0
* Add support for `help` parameter in *CLI arguments*:
* As `help` in *CLI arguments* is not supported by Click, there are two new internal classes (Click sub-classes) to support it:
* `typer.core.TyperArgument`
* `typer.core.TyperCommand`
* This includes a new auto-generated help text section `Arguments` for *CLI arguments*, showing defaults, required arguments, etc.
* It's also possible to disable it and keep the previous behavior, not showing automatic help for *CLI arguments* (Click's default) using the `hidden` parameter.
* Now `show_default` is `True` by default.
* And now `show_envvar` is `True` by default.
* So, default values and env vars are shown in the help text by default, without having to manually enable them, for both *CLI arguments* and *CLI options*.
* New docs:
* [CLI Arguments Intro](https://typer.tiangolo.com/tutorial/arguments/).
* [Optional CLI Arguments](https://typer.tiangolo.com/tutorial/arguments/optional/).
* [CLI Arguments with Default](https://typer.tiangolo.com/tutorial/arguments/default/).
* [CLI Arguments with Help](https://typer.tiangolo.com/tutorial/arguments/help/).
* [CLI Arguments with Environment Variables](https://typer.tiangolo.com/tutorial/arguments/envvar/).
* [CLI Arguments: Other uses](https://typer.tiangolo.com/tutorial/arguments/other-uses/).
* [CLI arguments with tuples](https://typer.tiangolo.com/tutorial/multiple-values/arguments-with-multiple-values/#cli-arguments-with-tuples).
* Lot's of tests for all the new examples in the new docs, keeping coverage at 100%.
* PR [#123](https://github.com/tiangolo/typer/pull/123).
* Add docs for calling packages with `python -m some_package` using `__main__.py`: [Building a Package: Support `python -m`](https://typer.tiangolo.com/tutorial/package/#support-python-m-optional). PR [#121](https://github.com/tiangolo/typer/pull/121).
* Add support for `*args` and `**kwargs` when calling the Typer app, just like in Click. PR [#120](https://github.com/tiangolo/typer/pull/120) by [@teymour-aldridge](https://github.com/teymour-aldridge).
* Fix typos in README and main docs [#103](https://github.com/tiangolo/typer/pull/103) by [@mrcartoonster](https://github.com/mrcartoonster).
* Fix typo in docs. PR [#98](https://github.com/tiangolo/typer/pull/98) by [@mrcartoonster](https://github.com/mrcartoonster).
* Fix typos and rewording in docs. PR [#97](https://github.com/tiangolo/typer/pull/97) by [@mrcartoonster](https://github.com/mrcartoonster).
* Update GitHub Action issue-manager. PR [#114](https://github.com/tiangolo/typer/pull/114).
## 0.2.1
* Add support for forward references (types declared inside of strings). PR [#93](https://github.com/tiangolo/typer/pull/93).
## 0.2.0
* Add support for completion for commands/programs not available on startup.
* This allows installing a Typer program/script in a virtual environment and still have completion globally installed.
* PR [#92](https://github.com/tiangolo/typer/pull/92).
* Add note about `typer.echo()` and `print()` for colors in Windows. PR [#89](https://github.com/tiangolo/typer/pull/89).
* Upgrade Mkdocs-Material version, update contributing guide style. PR [#90](https://github.com/tiangolo/typer/pull/90).
## 0.1.1
* Fix completion evaluation for Bash and Zsh when the program is not installed/found. PR [#83](https://github.com/tiangolo/typer/pull/83).
* Fix completion script for Fish. PR [#82](https://github.com/tiangolo/typer/pull/82).
* Fix shell installation for Bash to `~/.bashrc` and update Windows development docs. PR [#81](https://github.com/tiangolo/typer/pull/81).
* Update coverage badge. PR [#78](https://github.com/tiangolo/typer/pull/78).
## 0.1.0
* Fix coverage instructions. PR [#72](https://github.com/tiangolo/typer/pull/72).
* Add docs for [Building a Package](https://typer.tiangolo.com/tutorial/package/). PR [#71](https://github.com/tiangolo/typer/pull/71).
* Add docs for [Using Click (with Typer)](https://typer.tiangolo.com/tutorial/using-click/). PR [#70](https://github.com/tiangolo/typer/pull/70).
* Add support for type-based callbacks and autocompletion functions, extra tests and docs:
* Extra tests, raising coverage to 100%.
* New docs: [Printing and Colors: "Standard Output" and "Standard Error"](https://typer.tiangolo.com/tutorial/printing/#standard-output-and-standard-error).
* New docs: [Password CLI Option and Confirmation Prompt](https://typer.tiangolo.com/tutorial/options/password/).
* Support for callbacks based on type annotations. New docs: [CLI Option Callback and Context](https://typer.tiangolo.com/tutorial/options/callback-and-context/).
* New docs: [Version CLI Option, is_eager](https://typer.tiangolo.com/tutorial/options/version/).
* Support for autocompletion functions based on type annotations. New docs: [CLI Option autocompletion](https://typer.tiangolo.com/tutorial/options/autocompletion/).
* New docs: [Commands: Using the Context](https://typer.tiangolo.com/tutorial/commands/context/).
* New docs: [Testing](https://typer.tiangolo.com/tutorial/testing/).
* PR [#68](https://github.com/tiangolo/typer/pull/68).
* Fix Zsh completion install script. PR [#69](https://github.com/tiangolo/typer/pull/69).
* Fix typo in progressbar example. PR [#63](https://github.com/tiangolo/typer/pull/63) by [@ValentinCalomme](https://github.com/ValentinCalomme).
## 0.0.11
* Re-implement completion system:
* Remove optional dependency `click-completion` (with its sub-dependencies, like Jinja).
* Add optional dependency `shellingham` to auto detect shell to install (it was used by `click-completion`).
* Completion now doesn't require a third party library.
* If `shellingham` is not installed/added as a dependency, `--install-completion` and `--show-completion` take a value with the name of the shell.
* Fix support for user provided completion in *CLI Parameters*.
* Fix completion for files in Bash, Zsh, and Fish.
* Add support for modern versions of PowerShell, 5, 6, and 7 (e.g. in Windows 10).
* Add support for `pwsh` (PowerShell Core).
* PowerShell support includes help strings for commands and *CLI Parameters*.
* Several bug fixes.
* Tests for the completion logic/code.
* Tested in all the shells in Linux and Windows.
* PR [#66](https://github.com/tiangolo/typer/pull/66).
* Fix format in docs with highlighted lines. PR [#65](https://github.com/tiangolo/typer/pull/65).
* Add docs about [Typer CLI - completion for small scripts](https://typer.tiangolo.com/typer-cli/). PR [#64](https://github.com/tiangolo/typer/pull/64).
* Add docs about [Alternatives, Inspiration and Comparisons](https://typer.tiangolo.com/alternatives/). PR [#62](https://github.com/tiangolo/typer/pull/62).
* Add [Development - Contributing Guide](https://typer.tiangolo.com/contributing/). PR [#61](https://github.com/tiangolo/typer/pull/61).
## 0.0.10
* Add support for Click version 7.1.1. PR [#60](https://github.com/tiangolo/typer/pull/60).
## 0.0.9
* Add support for PEP 561, to allow `mypy` to type check applications built with **Typer**. PR [#58](https://github.com/tiangolo/typer/pull/58).
* Upgrade deploy docs to Netlify GitHub action. PR [#57](https://github.com/tiangolo/typer/pull/57).
* Add support for Mermaid JS for visualizations. PR [#56](https://github.com/tiangolo/typer/pull/56).
* Update CI to run docs deployment in GitHub actions. PR [#50](https://github.com/tiangolo/typer/pull/50).
* Update format for internal links. PR [#38](https://github.com/tiangolo/typer/pull/38).
* Tweak external links' format. PR [#36](https://github.com/tiangolo/typer/pull/36).
## 0.0.8
* Update docs and add latest changes to MkDocs/website. PR [#33](https://github.com/tiangolo/typer/pull/33).
* Add extra tests for edge cases that don't belong in docs' examples. PR [#32](https://github.com/tiangolo/typer/pull/32).
* Add docs for CLI Parameters with [Multiple Values](https://typer.tiangolo.com/tutorial/multiple-values/). Includes tests for all the examples and bug fixes. PR [#31](https://github.com/tiangolo/typer/pull/31).
* Add docs for extra *CLI parameter* types: [CLI Parameter Types: Number](https://typer.tiangolo.com/tutorial/parameter-types/number/) and [CLI Parameter Types: Boolean CLI Options](https://typer.tiangolo.com/tutorial/parameter-types/bool/). PR [#30](https://github.com/tiangolo/typer/pull/30).
* Extend docs for Commands, add [Commands: Typer Callback](https://typer.tiangolo.com/tutorial/commands/callback/) and [Commands: One or Multiple](https://typer.tiangolo.com/tutorial/commands/one-or-multiple/). This includes tests for all the examples and bug fixes. PR [#29](https://github.com/tiangolo/typer/pull/29).
* Add docs for [SubCommands - Command Groups](https://typer.tiangolo.com/tutorial/subcommands/). This includes tests for all the examples and bug fixes. PR [#28](https://github.com/tiangolo/typer/pull/28).
* Remove unneeded code for argument handling. PR [#26](https://github.com/tiangolo/typer/pull/26).
* Add docs for [Launching Applications](https://typer.tiangolo.com/tutorial/launch/). PR [#25](https://github.com/tiangolo/typer/pull/25).
* Add docs for getting the [CLI Application Directory](https://typer.tiangolo.com/tutorial/app-dir/). PR [#24](https://github.com/tiangolo/typer/pull/24).
* Add docs for [Progress Bars](https://typer.tiangolo.com/tutorial/progressbar/). PR [#23](https://github.com/tiangolo/typer/pull/23).
* Add docs for [Asking with Interactive Prompts](). PR [#22](https://github.com/tiangolo/typer/pull/22).
* Update docs for path *CLI option*. PR [#21](https://github.com/tiangolo/typer/pull/21).
* Add colors module and docs for [Printing and Colors](https://typer.tiangolo.com/tutorial/printing/) and for [Terminating](https://typer.tiangolo.com/tutorial/terminating/), including tests. PR [#20](https://github.com/tiangolo/typer/pull/20).
* Refactor docs to make each individual page/section "bite-sized" / small. Add docs for [CLI option names](https://typer.tiangolo.com/tutorial/options/name/). Update `typer.Argument()` to remove invalid positional `param_decls`. PR [#19](https://github.com/tiangolo/typer/pull/19).
## 0.0.7
* Add docs for [*CLI parameter* types](https://typer.tiangolo.com/tutorial/parameter-types/). Includes tests and file classes refactor. PR [#17](https://github.com/tiangolo/typer/pull/17).
* Add tests for completion. PR [#15](https://github.com/tiangolo/typer/pull/15) and [#16](https://github.com/tiangolo/typer/pull/16).
## 0.0.6
* Add docs for [Commands](https://typer.tiangolo.com/tutorial/commands/). Includes a bug fix for handling default values set in `typer.Typer()` parameters. PR [#14](https://github.com/tiangolo/typer/pull/14).
* Add docs for [CLI Arguments](https://typer.tiangolo.com/tutorial/arguments/). PR [#13](https://github.com/tiangolo/typer/pull/13).
* Add docs for [CLI Options](https://typer.tiangolo.com/tutorial/options/). PR [#12](https://github.com/tiangolo/typer/pull/12).
## 0.0.5
* Clean exports from Typer. Remove unneeded components from Click and add needed `Exit` exception. PR [#11](https://github.com/tiangolo/typer/pull/11).
* Fix and document extracting help from a function's docstring [First Steps: Document your CLI app](https://typer.tiangolo.com/tutorial/first-steps/#document-your-cli-app). PR [#10](https://github.com/tiangolo/typer/pull/10).
* Update references to `--install-completion` and `--show-completion` in docs. PR [#9](https://github.com/tiangolo/typer/pull/9).
* Fix testing utilities, add tests for First Steps examples. PR [#8](https://github.com/tiangolo/typer/pull/8).
* Add auto completion options by default when [click-completion](https://github.com/click-contrib/click-completion) is installed: `--install-completion` and `--show-completion`. PR [#7](https://github.com/tiangolo/typer/pull/7).
* Update Termynal to have fixed sizes, add "fast" button, and use it in [First Steps](https://typer.tiangolo.com/tutorial/first-steps/). PR [#6](https://github.com/tiangolo/typer/pull/6).
* Add custom automatic [Termynal](https://github.com/tiangolo/termynal) for docs. PR [#5](https://github.com/tiangolo/typer/pull/5).
## 0.0.4
* Update short descriptions and assets.
* Docs rewording and fix typos. PR [#1](https://github.com/tiangolo/typer/pull/1) by [@mariacamilagl](https://github.com/mariacamilagl).
## 0.0.3
* Fix group creation without name.
## 0.0.2
* Add initial version of code, docs, etc.
## 0.0.1
* First commit. Publish to PyPI to reserve package name.
================================================
FILE: docs/resources/index.md
================================================
# Resources
Additional resources, how to **help** and get help, how to **contribute**, and more. ✈️
================================================
FILE: docs/tutorial/app-dir.md
================================================
# CLI Application Directory
You can get the application directory where you can, for example, save configuration files with `typer.get_app_dir()`:
{* docs_src/app_dir/tutorial001_py310.py hl[12] *}
It will give you a directory for storing configurations appropriate for your CLI program for the current user in each operating system.
Check it:
```console
$ python main.py
Config file doesn't exist yet
```
## About `Path`
If you hadn't seen something like that:
```Python
Path(app_dir) / "config.json"
```
A `Path` object can be used with `/` and it will convert it to the separator for the current system (`/` for Unix systems and `\` for Windows).
If the first element is a `Path` object the next ones (after the `/`) can be `str`.
And it will create a new `Path` object from that.
If you want a quick guide on using `Path()` you can check this post on Real Python or this post by Trey Hunner.
In the code above, we are also explicitly declaring `config_path` as having type `Path` to help the editor provide completion and type checks:
```Python
config_path: Path = Path(app_dir) / "config.json"
```
Otherwise it could think it's a sub-type (a `PurePath`) and stop providing completion for some methods.
================================================
FILE: docs/tutorial/arguments/default.md
================================================
# CLI Arguments with Default
We can also use the same `typer.Argument()` to set a default value.
That way the *CLI argument* will be optional *and also* have a default value.
## An optional *CLI argument* with a default
We can also use `typer.Argument()` to make a *CLI argument* have a default value other than `None`:
{* docs_src/arguments/default/tutorial001_an_py310.py hl[9] *}
/// tip
Because now the value will be a `str` passed by the user or the default value of `"Wade Wilson"` which is also a `str`, we know the value will never be `None`, so we don't have to (and shouldn't) use `Optional[str]`.
Have in mind that the `Optional[something]` tells Python that a value "could be `None`". But the use of `Optional` doesn't affect Typer in any way, e.g. it doesn't tell Typer if a value is required or not.
///
Check it:
```console
// Check the help
$ python main.py --help
// Notice the [default: Wade Wilson] ✨
Usage: main.py [OPTIONS] [NAME]
Arguments:
[NAME] [default: Wade Wilson]
Options:
--help Show this message and exit.
// With no optional CLI argument
$ python main.py
Hello Wade Wilson
// With one CLI argument
$ python main.py Camila
Hello Camila
```
## Dynamic default value
And we can even make the default value be dynamically generated by passing a function as the `default_factory` argument:
{* docs_src/arguments/default/tutorial002_an_py310.py hl[9:10,14] *}
In this case, we created the function `get_name` that will just return a random `str` each time.
And we pass it as the first function argument to `typer.Argument()`.
/// tip
The word "factory" in `default_factory` is just a fancy way of saying "function that will create the default value".
///
Check it:
```console
// Check the help
$ python main.py --help
Usage: main.py [OPTIONS] [NAME]
Arguments:
[NAME] [default: (dynamic)]
Options:
--help Show this message and exit.
// Try it several times, it will use a random default each time
$ python main.py
Hello Deadpool
$ python main.py
Hello Hiro
$ python main.py
Hello Rick
// Now pass a value for the CLI argument
$ python main.py Camila
Hello Camila
```
================================================
FILE: docs/tutorial/arguments/envvar.md
================================================
# CLI Arguments with Environment Variables
You can also configure a *CLI argument* to read a value from an environment variable if it is not provided in the command line as a *CLI argument*.
/// tip
You can learn more about environment variables in the [Environment Variables](../../environment-variables.md){.internal-link target=_blank} page.
///
To do that, use the `envvar` parameter for `typer.Argument()`:
{* docs_src/arguments/envvar/tutorial001_an_py310.py hl[9] *}
In this case, the *CLI argument* `name` will have a default value of `"World"`, but will also read any value passed to the environment variable `AWESOME_NAME` if no value is provided in the command line:
```console
// Check the help
$ python main.py --help
Usage: main.py [OPTIONS] [NAME]
Arguments:
[NAME] [env var: AWESOME_NAME;default: World]
Options:
--help Show this message and exit.
// Call it without a CLI argument
$ python main.py
Hello Mr. World
// Now pass a value for the CLI argument
$ python main.py Czernobog
Hello Mr. Czernobog
// And now use the environment variable
$ AWESOME_NAME=Wednesday python main.py
Hello Mr. Wednesday
// CLI arguments take precedence over env vars
$ AWESOME_NAME=Wednesday python main.py Czernobog
Hello Mr. Czernobog
```
## Multiple environment variables
You are not restricted to a single environment variable, you can declare a list of environment variables that could be used to get a value if it was not passed in the command line:
{* docs_src/arguments/envvar/tutorial002_an_py310.py hl[10] *}
Check it:
```console
// Check the help
$ python main.py --help
Usage: main.py [OPTIONS] [NAME]
Arguments:
[NAME] [env var: AWESOME_NAME, GOD_NAME;default: World]
Options:
--help Show this message and exit.
// Try the first env var
$ AWESOME_NAME=Wednesday python main.py
Hello Mr. Wednesday
// Try the second env var
$ GOD_NAME=Anubis python main.py
Hello Mr. Anubis
```
## Hide an env var from the help text
By default, environment variables used will be shown in the help text, but you can disable them with `show_envvar=False`:
{* docs_src/arguments/envvar/tutorial003_an_py310.py hl[11] *}
Check it:
```console
//Check the help
$ python main.py --help
// It won't show the env var
Usage: main.py [OPTIONS] [NAME]
Arguments:
[NAME] [default: World]
Options:
--help Show this message and exit.
// But it will still be able to use it
$ AWESOME_NAME=Wednesday python main.py
Hello Mr. Wednesday
```
================================================
FILE: docs/tutorial/arguments/help.md
================================================
# CLI Arguments with Help
In the *First Steps* section you saw how to add help for a CLI app/command by adding it to a function's docstring.
Here's how that last example looked like:
{* docs_src/first_steps/tutorial006_py310.py *}
Now that you also know how to use `typer.Argument()`, let's use it to add documentation specific for a *CLI argument*.
## Add a `help` text for a *CLI argument*
You can use the `help` parameter to add a help text for a *CLI argument*:
{* docs_src/arguments/help/tutorial001_an_py310.py hl[9] *}
And it will be used in the automatic `--help` option:
```console
$ python main.py --help
// Check the section with Arguments below 🚀
Usage: main.py [OPTIONS] NAME
Arguments:
NAME The name of the user to greet [required]
Options:
--help Show this message and exit.
```
## Combine help text and docstrings
And of course, you can also combine that `help` with the docstring:
{* docs_src/arguments/help/tutorial002_an_py310.py hl[9:12] *}
And the `--help` option will combine all the information:
```console
$ python main.py --help
// Notice that we have the help text from the docstring and also the Arguments 📝
Usage: main.py [OPTIONS] NAME
Say hi to NAME very gently, like Dirk.
Arguments:
NAME The name of the user to greet [required]
Options:
--help Show this message and exit.
```
## Help with defaults
If you have a *CLI argument* with a default value, like `"World"`:
{* docs_src/arguments/help/tutorial003_an_py310.py hl[9] *}
It will show that default value in the help text:
```console
$ python main.py --help
// Notice the [default: World] 🔍
Usage: main.py [OPTIONS] [NAME]
Say hi to NAME very gently, like Dirk.
Arguments:
[NAME] Who to greet [default: World]
Options:
--help Show this message and exit.
```
But you can disable that if you want to, with `show_default=False`:
{* docs_src/arguments/help/tutorial004_an_py310.py hl[11] *}
And then it won't show the default value:
```console
$ python main.py --help
// Notice the there's no [default: World] now 🔥
Usage: main.py [OPTIONS] [NAME]
Say hi to NAME very gently, like Dirk.
Arguments:
[NAME] Who to greet
Options:
--help Show this message and exit.
```
## Custom default string
You can use the same `show_default` to pass a custom string (instead of a `bool`) to customize the default value to be shown in the help text:
{* docs_src/arguments/help/tutorial005_an_py310.py hl[13] *}
And it will be used in the help text:
```console
$ python main.py --help
Usage: main.py [OPTIONS] [NAME]
Arguments:
[NAME] Who to greet [default: (Deadpoolio the amazing's name)]
Options:
--help Show this message and exit.
// See it shows "(Deadpoolio the amazing's name)" instead of the actual default of "Wade Wilson"
```
## Custom help name (`metavar`)
You can also customize the text used in the generated help text to represent a *CLI argument*.
By default, it will be the same name you declared, in uppercase letters.
So, if you declare it as:
```Python
name: str
```
It will be shown as:
```
NAME
```
But you can customize it with the `metavar` parameter for `typer.Argument()`.
For example, let's say you don't want to have the default of `NAME`, you want to have `username`, in lowercase, and you really want ✨ emojis ✨ everywhere:
{* docs_src/arguments/help/tutorial006_an_py310.py hl[9] *}
Now the generated help text will have `✨username✨` instead of `NAME`:
```console
$ python main.py --help
Usage: main.py [OPTIONS] [✨username✨]
Arguments:
[✨username✨] [default: World]
Options:
--help Show this message and exit.
```
## *CLI Argument* help panels
You might want to show the help information for *CLI arguments* in different panels when using the `--help` option.
If you have installed Rich as described in the docs for [Printing and Colors](../printing.md){.internal-link target=_blank}, you can set the `rich_help_panel` parameter to the name of the panel where you want this *CLI argument* to be shown:
{* docs_src/arguments/help/tutorial007_an_py310.py hl[12,16] *}
Then, if you check the `--help` option, you will see a default panel named "`Arguments`" for the *CLI arguments* that don't have a custom `rich_help_panel`.
And next you will see other panels for the *CLI arguments* that have a custom panel set in the `rich_help_panel` parameter:
```console
$ python main.py --help
Usage: main.py [OPTIONS] NAME [LASTNAME] [AGE]
Say hi to NAME very gently, like Dirk.
╭─ Arguments ───────────────────────────────────────────────────────╮
│ * name TEXT Who to greet [default: None] [required] │
╰───────────────────────────────────────────────────────────────────╯
╭─ Secondary Arguments ─────────────────────────────────────────────╮
│ lastname [LASTNAME] The last name │
│ age [AGE] The user's age │
╰───────────────────────────────────────────────────────────────────╯
╭─ Options ─────────────────────────────────────────────────────────╮
│ --help Show this message and exit. │
╰───────────────────────────────────────────────────────────────────╯
```
In this example we have a custom *CLI arguments* panel named "`Secondary Arguments`".
## Help with style using Rich
In a future section you will see how to use custom markup in the `help` for *CLI arguments* when reading about [Commands - Command Help](../commands/help.md#rich-markdown-and-markup){.internal-link target=_blank}.
If you are in a hurry you can jump there, but otherwise, it would be better to continue reading here and following the tutorial in order.
## Hide a *CLI argument* from the help text
If you want, you can make a *CLI argument* **not** show up in the `Arguments` section in the help text.
You will probably not want to do this normally, but it's possible:
{* docs_src/arguments/help/tutorial008_an_py310.py hl[9] *}
Check it:
```console
$ python main.py --help
// Notice there's no Arguments section at all 🔥
Usage: main.py [OPTIONS] [NAME]
Say hi to NAME very gently, like Dirk.
Options:
--help Show this message and exit.
```
/// info
Have in mind that the *CLI argument* will still show up in the first line with `Usage`.
But it won't show up in the main help text under the `Arguments` section.
///
### Help text for *CLI arguments*
**Typer supports `help` for *CLI arguments*** to make it easier to have consistent help texts with a consistent format for your CLI programs. 🎨
This is also to help you create CLI programs that are ✨ awesome ✨ *by default*. With very little code.
If you don't want the CLI Argument to be shown in help outputs, you can set the `hidden` parameter to `True`.
================================================
FILE: docs/tutorial/arguments/index.md
================================================
# CLI Arguments
In the next few sections we'll see some ways to modify how *CLI arguments* work.
We'll create optional *CLI arguments*, we'll add integrated help for *CLI arguments*, etc.
================================================
FILE: docs/tutorial/arguments/optional.md
================================================
# Optional CLI Arguments
We said before that *by default*:
* *CLI options* are **optional**
* *CLI arguments* are **required**
Again, that's how they work *by default*, and that's the convention in many CLI programs and systems.
But you can change that.
In fact, it's very common to have **optional** *CLI arguments*, it's way more common than having **required** *CLI options*.
As an example of how it could be useful, let's see how the `ls` CLI program works.
```console
// If you just type
$ ls
// ls will "list" the files and directories in the current directory
typer tests README.md LICENSE
// But it also receives an optional CLI argument
$ ls ./tests/
// And then ls will list the files and directories inside of that directory from the CLI argument
__init__.py test_tutorial
```
## An alternative *CLI argument* declaration
In the [First Steps](../first-steps.md#add-a-cli-argument){.internal-link target=_blank} you saw how to add a *CLI argument*:
{* docs_src/first_steps/tutorial002_py310.py hl[4] *}
Now let's see an alternative way to create the same *CLI argument*:
{* docs_src/arguments/optional/tutorial000_an_py310.py hl[6] *}
Or, using an explicit `Typer()` instance creation:
{* docs_src/arguments/optional/tutorial001_an_py310.py hl[9] *}
/// info
Typer added support for `Annotated` (and started recommending it) in version 0.9.0.
If you have an older version, you would get errors when trying to use `Annotated`.
Make sure you upgrade the Typer version to at least 0.9.0 before using `Annotated`.
///
Before, you had this function parameter:
```Python
name: str
```
And now we wrap it with `Annotated`:
```Python
name: Annotated[str]
```
Both of these versions mean the same thing, `Annotated` is part of standard Python and is there for this.
But the second version using `Annotated` allows us to pass additional metadata that can be used by **Typer**:
```Python
name: Annotated[str, typer.Argument()]
```
Now we are being explicit that `name` is a *CLI argument*. It's still a `str` and it's still required (it doesn't have a default value).
All we did there achieves the same thing as before, a **required** *CLI argument*:
```console
$ python main.py
Usage: main.py [OPTIONS] NAME
Try "main.py --help" for help.
Error: Missing argument 'NAME'.
```
It's still not very useful, but it works correctly.
And being able to declare a **required** *CLI argument* using
```Python
name: Annotated[str, typer.Argument()]
```
...that works exactly the same as
```Python
name: str
```
...will come handy later.
## Make an optional *CLI argument*
Now, finally what we came for, an optional *CLI argument*.
To make a *CLI argument* optional, use `typer.Argument()` and make sure to provide a "default" value, for example `"World"`:
{* docs_src/arguments/optional/tutorial002_an_py310.py hl[9] *}
Now we have:
```Python
name: Annotated[str, typer.Argument()] = "World"
```
Because we are using `typer.Argument()` **Typer** will know that this is a *CLI argument* (no matter if *required* or *optional*).
Check the help:
```console
// First check the help
$ python main.py --help
Usage: main.py [OPTIONS] [NAME]
Arguments:
[NAME]
Options:
--help Show this message and exit.
```
/// tip
Notice that `NAME` is still a *CLI argument*, it's shown up there in the "`Usage: main.py` ...".
Also notice that now `[NAME]` has brackets ("`[`" and "`]`") around (before it was just `NAME`) to denote that it's **optional**, not **required**.
///
Now run it and test it:
```console
// With no CLI argument
$ python main.py
Hello World!
// With one optional CLI argument
$ python main.py Camila
Hello Camila
```
/// tip
Notice that "`Camila`" here is an optional *CLI argument*, not a *CLI option*, because we didn't use something like "`--name Camila`", we just passed "`Camila`" directly to the program.
///
## Alternative (old) `typer.Argument()` as the default value
**Typer** also supports another older alternative syntax for declaring *CLI arguments* with additional metadata.
Instead of using `Annotated`, you can use `typer.Argument()` as the default value:
{* docs_src/arguments/optional/tutorial001_py310.py hl[7] *}
/// tip
Prefer to use the `Annotated` version if possible.
///
Before, because `name` didn't have any default value it would be a **required parameter** for the Python function, in Python terms.
When using `typer.Argument()` as the default value **Typer** does the same and makes it a **required** *CLI argument*.
We changed it to:
```Python
name: str = typer.Argument()
```
But now as `typer.Argument()` is the "default value" of the function's parameter, it would mean that "it is no longer required" (in Python terms).
As we no longer have the Python function default value (or its absence) to tell if something is required or not and what is the default value, `typer.Argument()` receives a first parameter `default` that serves the same purpose of defining that default value, or making it required.
Not passing any value to the `default` argument is the same as marking it as required. But you can also explicitly mark it as *required* by passing `...` as the `default` argument, passed to `typer.Argument(default=...)`.
```Python
name: str = typer.Argument(default=...)
```
/// info
If you hadn't seen that `...` before: it is a special single value, it is part of Python and is called "Ellipsis".
///
{* docs_src/arguments/optional/tutorial003_py310.py hl[7] *}
And the same way, you can make it optional by passing a different `default` value, for example `"World"`:
{* docs_src/arguments/optional/tutorial002_py310.py hl[7] *}
Because the first parameter passed to `typer.Argument(default="World")` (the new "default" value) is `"World"`, **Typer** knows that this is an **optional** *CLI argument*, if no value is provided when calling it in the command line, it will have that default value of `"World"`.
The `default` argument is the first one, so it's possible that you see code that passes the value without explicitly using `default=`, like:
```Python
name: str = typer.Argument(...)
```
...or like:
```Python
name: str = typer.Argument("World")
```
...but again, try to use `Annotated` if possible, that way your code in terms of Python will mean the same thing as with **Typer** and you won't have to remember any of these details.
================================================
FILE: docs/tutorial/arguments/other-uses.md
================================================
# Other uses
`typer.Argument()` has several other use cases. Such as for data validation, to enable other features, etc.
You will see about these use cases later in the docs.
================================================
FILE: docs/tutorial/commands/arguments.md
================================================
# Command CLI Arguments
The same way as with a CLI application with a single command, subcommands (or just "commands") can also have their own *CLI arguments*:
{* docs_src/commands/arguments/tutorial001_py310.py hl[7,12] *}
```console
// Check the help for create
$ python main.py create --help
Usage: main.py create [OPTIONS] USERNAME
Options:
--help Show this message and exit.
// Call it with a CLI argument
$ python main.py create Camila
Creating user: Camila
// The same for delete
$ python main.py delete Camila
Deleting user: Camila
```
/// tip
Everything to the *right* of the *command* are *CLI parameters* (*CLI arguments* and *CLI options*) for that command.
///
/// note | Technical Details
Actually, it's everything to the right of that command, *before any subcommand*.
It's possible to have groups of *subcommands*, it's like if one *command* also had *subcommands*. And then those *subcommands* could have their own *CLI parameters*, taking their own *CLI parameters*.
You will see about them later in another section.
///
================================================
FILE: docs/tutorial/commands/callback.md
================================================
# Typer Callback
When you create an `app = typer.Typer()` it works as a group of commands.
And you can create multiple commands with it.
Each of those commands can have their own *CLI parameters*.
But as those *CLI parameters* are handled by each of those commands, they don't allow us to create *CLI parameters* for the main CLI application itself.
But we can use `@app.callback()` for that.
It's very similar to `@app.command()`, but it declares the *CLI parameters* for the main CLI application (before the commands):
{* docs_src/commands/callback/tutorial001_py310.py hl[25,26,27,28,29,30,31,32] *}
Here we create a `callback` with a `--verbose` *CLI option*.
/// tip
After getting the `--verbose` flag, we modify a global `state`, and we use it in the other commands.
There are other ways to achieve the same, but this will suffice for this example.
///
And as we added a docstring to the callback function, by default it will be extracted and used as the help text.
Check it:
```console
// Check the help
$ python main.py --help
// Notice the main help text, extracted from the callback function: "Manage users in the awesome CLI app."
Usage: main.py [OPTIONS] COMMAND [ARGS]...
Manage users in the awesome CLI app.
Options:
--verbose / --no-verbose [default: False]
--install-completion Install completion for the current shell.
--show-completion Show completion for the current shell, to copy it or customize the installation.
--help Show this message and exit.
Commands:
create
delete
// Check the new top level CLI option --verbose
// Try it normally
$ python main.py create Camila
Creating user: Camila
// And now with --verbose
$ python main.py --verbose create Camila
Will write verbose output
About to create a user
Creating user: Camila
Just created a user
// Notice that --verbose belongs to the callback, it has to go before create or delete ⛔️
$ python main.py create --verbose Camila
Usage: main.py create [OPTIONS] USERNAME
Try "main.py create --help" for help.
Error: No such option: --verbose
```
## Adding a callback on creation
It's also possible to add a callback when creating the `typer.Typer()` app:
{* docs_src/commands/callback/tutorial002_py310.py hl[4,5,8] *}
That achieves the same as with `@app.callback()`.
Check it:
```console
$ python main.py create Camila
Running a command
Creating user: Camila
```
## Overriding a callback
If you added a callback when creating the `typer.Typer()` app, it's possible to override it with `@app.callback()`:
{* docs_src/commands/callback/tutorial003_py310.py hl[11,12,13] *}
Now `new_callback()` will be the one used.
Check it:
```console
$ python main.py create Camila
// Notice that the message is the one from new_callback()
Override callback, running a command
Creating user: Camila
```
## Adding a callback only for documentation
You can also add a callback just to add the documentation in the docstring.
It can be convenient especially if you have several lines of text, as the indentation will be automatically handled for you:
{* docs_src/commands/callback/tutorial004_py310.py hl[8,9,10,11,12,13,14,15,16] *}
Now the callback will be used mainly to extract the docstring for the help text.
Check it:
```console
$ python main.py --help
// Notice all the help text extracted from the callback docstring
Usage: main.py [OPTIONS] COMMAND [ARGS]...
Manage users CLI app.
Use it with the create command.
A new user with the given NAME will be created.
Options:
--install-completion Install completion for the current shell.
--show-completion Show completion for the current shell, to copy it or customize the installation.
--help Show this message and exit.
Commands:
create
// And it just works as normally
$ python main.py create Camila
Creating user: Camila
```
================================================
FILE: docs/tutorial/commands/context.md
================================================
# Using the Context
When you create a **Typer** application it always has a special, hidden object underneath called the "Context".
But you can access the context by declaring a function parameter of type `typer.Context`.
You might have read it in [CLI Option Callback and Context](../options/callback-and-context.md){.internal-link target=_blank}.
The same way, in commands or in the main `Typer` callback you can access the context by declaring a function parameter of type `typer.Context`.
## Getting the context
For example, let's say that you want to execute some logic in a `Typer` callback depending on the subcommand that is being called.
You can get the name of the subcommand from the context:
{* docs_src/commands/context/tutorial001_py310.py hl[17,21] *}
Check it:
```console
$ python main.py create Camila
// We get the message from the callback
About to execute command: create
Creating user: Camila
$ python main.py delete Camila
// We get the message from the callback, this time with delete
About to execute command: delete
Deleting user: Camila
```
## Executable callback
By default, the callback is only executed right before executing a command.
And if no command is provided, the help message is shown.
But we could make it run even without a subcommand with `invoke_without_command=True`:
{* docs_src/commands/context/tutorial002_py310.py hl[16] *}
Check it:
```console
$ python main.py
// The callback is executed, we don't get the default help message
Initializing database
// Try with a command
$ python main.py create Camila
// The callback is still executed
Initializing database
Creating user: Camila
```
## Exclusive executable callback
We might not want the callback to be executed if there's already other command that will be executed.
For that, we can get the `typer.Context` and check if there's an invoked command in `ctx.invoked_subcommand`.
If it's `None`, it means that we are not calling a subcommand but the main program (the callback) directly:
{* docs_src/commands/context/tutorial003_py310.py hl[17,21] *}
Check it:
```console
$ python main.py
// The callback is executed
Initializing database
// Check it with a subcommand
$ python main.py create Camila
// This time the callback is not executed
Creating user: Camila
```
## Configuring the context
You can pass configurations for the context when creating a command or callback.
For example, you could keep additional *CLI parameters* not declared in your CLI program with `ignore_unknown_options` and `allow_extra_args`.
Then you can access those extra raw *CLI parameters* as a `list` of `str` in `ctx.args`:
{* docs_src/commands/context/tutorial004_py310.py hl[7,9,10] *}
```console
$ python main.py --name Camila --city Berlin
Got extra arg: --name
Got extra arg: Camila
Got extra arg: --city
Got extra arg: Berlin
```
/// tip
Notice that it saves all the extra *CLI parameters* as a raw `list` of `str`, including the *CLI option* names and values, everything together.
///
================================================
FILE: docs/tutorial/commands/help.md
================================================
# Command Help
The same as before, you can add help for the commands in the docstrings and the *CLI options*.
And the `typer.Typer()` application receives a parameter `help` that you can pass with the main help text for your CLI program:
{* docs_src/commands/help/tutorial001_an_py310.py hl[5,10:12,23,27:31,44,48:52,61:63] *}
Check it:
```console
// Check the new help
$ python main.py --help
Usage: main.py [OPTIONS] COMMAND [ARGS]...
Awesome CLI user manager.
Options:
--install-completion Install completion for the current shell.
--show-completion Show completion for the current shell, to copy it or customize the installation.
--help Show this message and exit.
Commands:
create Create a new user with USERNAME.
delete Delete a user with USERNAME.
delete-all Delete ALL users in the database.
init Initialize the users database.
// Now the commands have inline help 🎉
// Check the help for create
$ python main.py create --help
Usage: main.py create [OPTIONS] USERNAME
Create a new user with USERNAME.
Options:
--help Show this message and exit.
// Check the help for delete
$ python main.py delete --help
Usage: main.py delete [OPTIONS] USERNAME
Delete a user with USERNAME.
If --force is not used, will ask for confirmation.
Options:
--force / --no-force Force deletion without confirmation. [required]
--help Show this message and exit.
// Check the help for delete-all
$ python main.py delete-all --help
Usage: main.py delete-all [OPTIONS]
Delete ALL users in the database.
If --force is not used, will ask for confirmation.
Options:
--force / --no-force Force deletion without confirmation. [required]
--help Show this message and exit.
// Check the help for init
$ python main.py init --help
Usage: main.py init [OPTIONS]
Initialize the users database.
Options:
--help Show this message and exit.
```
/// tip
`typer.Typer()` receives several other parameters for other things, we'll see that later.
You will also see how to use "Callbacks" later, and those include a way to add this same help message in a function docstring.
///
## Overwrite command help
You will probably be better adding the help text as a docstring to your functions, but if for some reason you wanted to overwrite it, you can use the `help` function argument passed to `@app.command()`:
{* docs_src/commands/help/tutorial002_py310.py hl[6,14] *}
Check it:
```console
// Check the help
$ python main.py --help
// Notice it uses the help passed to @app.command()
Usage: main.py [OPTIONS] COMMAND [ARGS]...
Options:
--install-completion Install completion for the current shell.
--show-completion Show completion for the current shell, to copy
it or customize the installation.
--help Show this message and exit.
Commands:
create Create a new user with USERNAME.
delete Delete a user with USERNAME.
// It uses "Create a new user with USERNAME." instead of "Some internal utility function to create."
```
## Deprecate a Command
There could be cases where you have a command in your app that you need to deprecate, so that your users stop using it, even while it's still supported for a while.
You can mark it with the parameter `deprecated=True`:
{* docs_src/commands/help/tutorial003_py310.py hl[14] *}
And when you show the `--help` option you will see it's marked as "`deprecated`":
```console
$ python main.py --help
Usage: main.py [OPTIONS] COMMAND [ARGS]...
╭─ Options ─────────────────────────────────────────────────────────╮
│ --install-completion Install completion for the current │
│ shell. │
│ --show-completion Show completion for the current │
│ shell, to copy it or customize the │
│ installation. │
│ --help Show this message and exit. │
╰───────────────────────────────────────────────────────────────────╯
╭─ Commands ────────────────────────────────────────────────────────╮
│ create Create a user. │
│ delete Delete a user. (deprecated) │
╰───────────────────────────────────────────────────────────────────╯
```
And if you check the `--help` for the deprecated command (in this example, the command `delete`), it also shows it as deprecated:
```console
$ python main.py delete --help
Usage: main.py delete [OPTIONS] USERNAME
(deprecated)
Delete a user.
This is deprecated and will stop being supported soon.
╭─ Arguments ───────────────────────────────────────────────────────╮
│ * username TEXT [default: None] [required] │
╰───────────────────────────────────────────────────────────────────╯
╭─ Options ─────────────────────────────────────────────────────────╮
│ --help Show this message and exit. │
╰───────────────────────────────────────────────────────────────────╯
```
## Suggest Commands
As of version 0.20.0, Typer added support for suggesting mistyped command names. This feature is **enabled by default**, but you can disable it with the parameter `suggest_commands=False`:
{* docs_src/commands/index/tutorial005_py310.py hl[3] *}
If a user mistypes a command, they'll see a helpful suggestion:
```console
$ python main.py crate
Usage: main.py [OPTIONS] COMMAND [ARGS]...
Try 'main.py --help' for help.
╭─ Error ───────────────────────────────────────────────────────────╮
│ No such command 'crate'. Did you mean 'create'? │
╰───────────────────────────────────────────────────────────────────╯
```
If there are multiple close matches, Typer will suggest them all. This feature uses Python's built-in `difflib.get_close_matches()` to find similar command names, making your CLI more user-friendly by helping users recover from typos.
## Rich Markdown and Markup
Typer installs **Rich** to allow for more formatting in the docstrings and the `help` parameter for *CLI arguments* and *CLI options*. You will see more about it below. 👇
/// info
You can disable rich text formatting by setting `rich_markup_mode` to `None` for your specific app.
Alternatively, you can disable it globally using an environmental variable `TYPER_USE_RICH` set to `False` or `0`.
///
### Rich Markup
If you set `rich_markup_mode="rich"` when creating the `typer.Typer()` app (which is the default), you will be able to use Rich Console Markup in the docstring, and even in the help for the *CLI arguments* and options:
{* docs_src/commands/help/tutorial004_an_py310.py hl[5,11,15:17,22,25,28] *}
With that, you can use Rich Console Markup to format the text in the docstring for the command `create`, make the word "`create`" bold and green, and even use an emoji.
You can also use markup in the help for the `username` CLI Argument.
And the same as before, the help text overwritten for the command `delete` can also use Rich Markup, the same in the CLI Argument and CLI Option.
If you run the program and check the help, you will see that **Typer** uses **Rich** internally to format the help.
Check the help for the `create` command:
```console
$ python main.py create --help
Usage: main.py create [OPTIONS] USERNAME
Create a new shiny user. ✨
This requires a username.
╭─ Arguments ───────────────────────────────────────────────────────╮
│ * username TEXT The username to be created │
│ [default: None] │
│ [required] │
╰───────────────────────────────────────────────────────────────────╯
╭─ Options ─────────────────────────────────────────────────────────╮
│ --help Show this message and exit. │
╰───────────────────────────────────────────────────────────────────╯
```
And check the help for the `delete` command:
```console
$ python main.py delete --help
Usage: main.py delete [OPTIONS] USERNAME
Delete a user with USERNAME.
╭─ Arguments ───────────────────────────────────────────────────────╮
│ * username TEXT The username to be deleted │
│ [default: None] │
│ [required] │
╰───────────────────────────────────────────────────────────────────╯
╭─ Options ─────────────────────────────────────────────────────────╮
│ --force --no-force Force the deletion 💥 │
│ [default: no-force] │
│ --help Show this message and exit. │
╰───────────────────────────────────────────────────────────────────╯
```
### Rich Markdown
If you set `rich_markup_mode="markdown"` when creating the `typer.Typer()` app, you will be able to use Markdown in the docstring:
{* docs_src/commands/help/tutorial005_an_py310.py hl[5,10,13:21,26,28:29] *}
With that, you can use Markdown to format the text in the docstring for the command `create`, make the word "`create`" bold, show a list of items, and even use an emoji.
And the same as before, the help text overwritten for the command `delete` can also use Markdown.
Check the help for the `create` command:
```console
$ python main.py create --help
Usage: main.py create [OPTIONS] USERNAME
Create a new shiny user. ✨
• Create a username
• Show that the username is created
───────────────────────────────────────────────────────────────────
Learn more at the Typer docs website
╭─ Arguments ───────────────────────────────────────────────────────╮
│ * username TEXT The username to be created │
│ [default: None] │
│ [required] │
╰───────────────────────────────────────────────────────────────────╯
╭─ Options ─────────────────────────────────────────────────────────╮
│ --help Show this message and exit. │
╰───────────────────────────────────────────────────────────────────╯
```
And the same for the `delete` command:
```console
$ python main.py delete --help
Usage: main.py delete [OPTIONS] USERNAME
Delete a user with USERNAME.
╭─ Arguments ───────────────────────────────────────────────────────╮
│ * username TEXT The username to be deleted │
│ [default: None] │
│ [required] │
╰───────────────────────────────────────────────────────────────────╯
╭─ Options ─────────────────────────────────────────────────────────╮
│ --force --no-force Force the deletion 💥 │
│ [default: no-force] │
│ --help Show this message and exit. │
╰───────────────────────────────────────────────────────────────────╯
```
/// info
Notice that in Markdown you cannot define colors. For colors you might prefer to use Rich markup.
///
## Help Panels
If you have many commands or CLI parameters, you might want to show their documentation in different panels when using the `--help` option.
If you installed Rich as described in [Printing and Colors](../printing.md){.internal-link target=_blank}, you can configure the panel to use for each command or CLI parameter.
### Help Panels for Commands
To set the panel for a command you can pass the argument `rich_help_panel` with the name of the panel you want to use:
{* docs_src/commands/help/tutorial006_py310.py hl[22,30,38,46] *}
Commands without a panel will be shown in the default panel `Commands`, and the rest will be shown in the next panels:
```console
$ python main.py --help
Usage: main.py [OPTIONS] COMMAND [ARGS]...
╭─ Options ─────────────────────────────────────────────────────────╮
│ --install-completion Install completion for the current │
│ shell. │
│ --show-completion Show completion for the current │
│ shell, to copy it or customize the │
│ installation. │
│ --help Show this message and exit. │
╰───────────────────────────────────────────────────────────────────╯
╭─ Commands ────────────────────────────────────────────────────────╮
│ create Create a new user. ✨ │
│ delete Delete a user. ❌ │
╰───────────────────────────────────────────────────────────────────╯
╭─ Utils and Configs ───────────────────────────────────────────────╮
│ config Configure the system. ⚙ │
│ sync Synchronize the system or something fancy like that. ♻ │
╰───────────────────────────────────────────────────────────────────╯
╭─ Help and Others ─────────────────────────────────────────────────╮
│ help Get help with the system. ❓ │
│ report Report an issue. ❗ │
╰───────────────────────────────────────────────────────────────────╯
```
### Help Panels for CLI Parameters
The same way, you can configure the panels for *CLI arguments* and *CLI options* with `rich_help_panel`.
And of course, in the same application you can also set the `rich_help_panel` for commands.
{* docs_src/commands/help/tutorial007_an_py310.py hl[14,20,26,36] *}
Then if you run the application you will see all the *CLI parameters* in their respective panels.
* First the ***CLI arguments*** that don't have a panel name set in a **default** one named "`Arguments`".
* Next the ***CLI arguments*** with a **custom panel**. In this example named "`Secondary Arguments`".
* After that, the ***CLI options*** that don't have a panel in a **default** one named "`Options`".
* And finally, the ***CLI options*** with a **custom panel** set. In this example named "`Additional Data`".
You can check the `--help` option for the command `create`:
```console
$ python main.py create --help
Usage: main.py create [OPTIONS] USERNAME [LASTNAME]
Create a new user. ✨
╭─ Arguments ───────────────────────────────────────────────────────╮
│ * username TEXT The username to create [default: None] │
│ [required] │
╰───────────────────────────────────────────────────────────────────╯
╭─ Secondary Arguments ─────────────────────────────────────────────╮
│ lastname [LASTNAME] The last name of the new user │
╰───────────────────────────────────────────────────────────────────╯
╭─ Options ─────────────────────────────────────────────────────────╮
│ --force --no-force Force the creation of the user │
│ [default: no-force] │
│ --help Show this message and exit. │
╰───────────────────────────────────────────────────────────────────╯
╭─ Additional Data ─────────────────────────────────────────────────╮
│ --age INTEGER The age of the new user │
│ [default: None] │
│ --favorite-color TEXT The favorite color of the new │
│ user │
│ [default: None] │
╰───────────────────────────────────────────────────────────────────╯
```
And of course, the `rich_help_panel` can be used in the same way for commands in the same application.
And those panels will be shown when you use the main `--help` option.
```console
$ python main.py --help
Usage: main.py [OPTIONS] COMMAND [ARGS]...
╭─ Options ─────────────────────────────────────────────────────────╮
│ --install-completion Install completion for the current │
│ shell. │
│ --show-completion Show completion for the current │
│ shell, to copy it or customize the │
│ installation. │
│ --help Show this message and exit. │
╰───────────────────────────────────────────────────────────────────╯
╭─ Commands ────────────────────────────────────────────────────────╮
│ create Create a new user. ✨ │
╰───────────────────────────────────────────────────────────────────╯
╭─ Utils and Configs ───────────────────────────────────────────────╮
│ config Configure the system. ⚙ │
╰───────────────────────────────────────────────────────────────────╯
```
You can see the custom panel for the commands for "`Utils and Configs`".
## Epilog
If you need, you can also add an epilog section to the help of your commands:
{* docs_src/commands/help/tutorial008_py310.py hl[6] *}
And when you check the `--help` option it will look like:
```console
$ python main.py --help
Usage: main.py [OPTIONS] USERNAME
Create a new user. ✨
╭─ Arguments ───────────────────────────────────────────────────────╮
│ * username TEXT [default: None] [required] │
╰───────────────────────────────────────────────────────────────────╯
╭─ Options ─────────────────────────────────────────────────────────╮
│ --install-completion Install completion for the current │
│ shell. │
│ --show-completion Show completion for the current │
│ shell, to copy it or customize the │
│ installation. │
│ --help Show this message and exit. │
╰───────────────────────────────────────────────────────────────────╯
Made with ❤ in Venus
```
================================================
FILE: docs/tutorial/commands/index.md
================================================
# Commands
We have seen how to create a CLI program with possibly several *CLI options* and *CLI arguments*.
But **Typer** allows you to create CLI programs with several commands (also known as subcommands).
For example, the program `git` has several commands.
One command of `git` is `git push`. And `git push` in turn takes its own *CLI arguments* and *CLI options*.
For example:
```console
// The push command with no parameters
$ git push
---> 100%
// The push command with one CLI option --set-upstream and 2 CLI arguments
$ git push --set-upstream origin master
---> 100%
```
Another command of `git` is `git pull`, it also has some *CLI parameters*.
It's like if the same big program `git` had several small programs inside.
/// tip
A command looks the same as a *CLI argument*, it's just some name without a preceding `--`. But commands have a predefined name, and are used to group different sets of functionalities into the same CLI application.
///
## Command or subcommand
It's common to call a CLI program a "command".
But when one of these programs have subcommands, those subcommands are also frequently called just "commands".
Have that in mind so you don't get confused.
Here I'll use **CLI application** or **program** to refer to the program you are building in Python with Typer, and **command** to refer to one of these "subcommands" of your program.
## A CLI application with multiple commands
**Typer** allows creating CLI applications with multiple commands/subcommands.
Now that you know how to create an explicit `typer.Typer()` application and add one command, let's see how to add multiple commands.
Let's say that we have a CLI application to manage users.
We'll have a command to `create` users and another command to `delete` them.
To begin, let's say it can only create and delete one single predefined user:
{* docs_src/commands/index/tutorial002_py310.py hl[6,11] *}
Now we have a CLI application with 2 commands, `create` and `delete`:
```console
// Check the help
$ python main.py --help
Usage: main.py [OPTIONS] COMMAND [ARGS]...
Options:
--install-completion Install completion for the current shell.
--show-completion Show completion for the current shell, to copy it or customize the installation.
--help Show this message and exit.
Commands:
create
delete
// Test them
$ python main.py create
Creating user: Hiro Hamada
$ python main.py delete
Deleting user: Hiro Hamada
// Now we have 2 commands! 🎉
```
Notice that the help text now shows the 2 commands: `create` and `delete`.
/// tip
By default, the names of the commands are generated from the function name.
///
## Show the help message if no command is given
By default, we need to specify `--help` to get the command's help page.
However, by setting `no_args_is_help=True` when defining the `typer.Typer()` application, the help function will be shown whenever no argument is given:
{* docs_src/commands/index/tutorial003_py310.py hl[3] *}
Now we can run this:
```console
// Check the help without having to type --help
$ python main.py
Usage: main.py [OPTIONS] COMMAND [ARGS]...
Options:
--install-completion Install completion for the current shell.
--show-completion Show completion for the current shell, to copy it or customize the installation.
--help Show this message and exit.
Commands:
create
delete
```
## Sorting of the commands
Note that by design, **Typer** shows the commands in the order they've been declared.
So, if we take our original example, with `create` and `delete` commands, and reverse the order in the Python file:
{* docs_src/commands/index/tutorial004_py310.py hl[7,12] *}
Then we will see the `delete` command first in the help output:
```console
// Check the help
$ python main.py --help
Usage: main.py [OPTIONS] COMMAND [ARGS]...
Options:
--install-completion Install completion for the current shell.
--show-completion Show completion for the current shell, to copy it or customize the installation.
--help Show this message and exit.
Commands:
delete
create
```
## Decorator Technical Details
When you use `@app.command()` the function under the decorator is registered in the **Typer** application and is then used later by the application.
But Typer doesn't modify that function itself, the function is left as is.
That means that if your function is simple enough that you could create it without using `typer.Option()` or `typer.Argument()`, you could use the same function for a **Typer** application and a **FastAPI** application putting both decorators on top, or similar tricks.
================================================
FILE: docs/tutorial/commands/name.md
================================================
# Custom Command Name
By default, the command names are generated from the function name.
So, if your function is something like:
```Python
def create(username: str):
...
```
Then the command name will be `create`.
But if you already had a function called `create()` somewhere in your code, you would have to name your CLI function differently.
And what if you wanted the command to still be named `create`?
For this, you can set the name of the command in the first parameter for the `@app.command()` decorator:
{* docs_src/commands/name/tutorial001_py310.py hl[6,11] *}
Now, even though the functions are named `cli_create_user()` and `cli_delete_user()`, the commands will still be named `create` and `delete`:
```console
$ python main.py --help
Usage: main.py [OPTIONS] COMMAND [ARGS]...
Options:
--install-completion Install completion for the current shell.
--show-completion Show completion for the current shell, to copy it or customize the installation.
--help Show this message and exit.
Commands:
create
delete
// Test it
$ python main.py create Camila
Creating user: Camila
```
Note that any underscores in the function name will be replaced with dashes.
So if your function is something like:
```Python
def create_user(username: str):
...
```
Then the command name will be `create-user`.
================================================
FILE: docs/tutorial/commands/one-or-multiple.md
================================================
# One or Multiple Commands
You might have noticed that if you create a single command, as in the following example:
{* docs_src/typer_app/tutorial001_py310.py hl[3,6,12] *}
**Typer** is smart enough to create a CLI application with that single function as the main CLI application, not as a command/subcommand:
```console
// Without a CLI argument
$ python main.py
Usage: main.py [OPTIONS] NAME
Try "main.py --help" for help.
Error: Missing argument 'NAME'.
// With the NAME CLI argument
$ python main.py Camila
Hello Camila
// Asking for help
$ python main.py
Usage: main.py [OPTIONS] NAME
Options:
--install-completion Install completion for the current shell.
--show-completion Show completion for the current shell, to copy it or customize the installation.
--help Show this message and exit.
```
/// tip
Notice that it doesn't show a command `main`, even though the function name is `main`.
///
But if you add multiple commands, **Typer** will create one *CLI command* for each one of them:
{* docs_src/commands/index/tutorial002_py310.py hl[6,11] *}
Here we have 2 commands `create` and `delete`:
```console
// Check the help
$ python main.py --help
Usage: main.py [OPTIONS] COMMAND [ARGS]...
Options:
--install-completion Install completion for the current shell.
--show-completion Show completion for the current shell, to copy it or customize the installation.
--help Show this message and exit.
Commands:
create
delete
// Test the commands
$ python main.py create
Creating user: Hiro Hamada
$ python main.py delete
Deleting user: Hiro Hamada
```
## One command and one callback
If you want to create a CLI app with one single command but you still want it to be a command/subcommand you can just add a callback:
{* docs_src/commands/one_or_multiple/tutorial001_py310.py hl[11,12,13] *}
And now your CLI program will have a single command.
Check it:
```console
// Check the help
$ python main.py --help
// Notice the single command create
Usage: main.py [OPTIONS] COMMAND [ARGS]...
Options:
--install-completion Install completion for the current shell.
--show-completion Show completion for the current shell, to copy it or customize the installation.
--help Show this message and exit.
Commands:
create
// Try it
$ python main.py create
Creating user: Hiro Hamada
```
## Using the callback to document
Now that you are using a callback just to have a single command, you might as well use it to add documentation for your app:
{* docs_src/commands/one_or_multiple/tutorial002_py310.py hl[11,12,13,14,15,16,17] *}
And now the docstring from the callback will be used as the help text:
```console
$ python main.py --help
// Notice the help text from the docstring
Usage: main.py [OPTIONS] COMMAND [ARGS]...
Creates a single user Hiro Hamada.
In the next version it will create 5 more users.
Options:
--install-completion Install completion for the current shell.
--show-completion Show completion for the current shell, to copy it or customize the installation.
--help Show this message and exit.
Commands:
create
// And it still works the same, the callback does nothing
$ python main.py create
Creating user: Hiro Hamada
```
================================================
FILE: docs/tutorial/commands/options.md
================================================
# Command CLI Options
Commands can also have their own *CLI options*.
In fact, each command can have different *CLI arguments* and *CLI options*:
{* docs_src/commands/options/tutorial001_an_py310.py hl[9,15:18,28:30,39] *}
Here we have multiple commands, with different *CLI parameters*:
* `create`:
* `username`: a *CLI argument*.
* `delete`:
* `username`: a *CLI argument*.
* `--force`: a *CLI option*, if not provided, it's prompted.
* `delete-all`:
* `--force`: a *CLI option*, if not provided, it's prompted.
* `init`:
* Doesn't take any *CLI parameters*.
```console
// Check the help
python main.py --help
Usage: main.py [OPTIONS] COMMAND [ARGS]...
Options:
--install-completion Install completion for the current shell.
--show-completion Show completion for the current shell, to copy it or customize the installation.
--help Show this message and exit.
Commands:
create
delete
delete-all
init
```
/// tip
Check the command `delete-all`, by default command names are generated from the function name, replacing `_` with `-`.
///
Test it:
```console
// Check the command create
$ python main.py create Camila
Creating user: Camila
// Now test the command delete
$ python main.py delete Camila
# Are you sure you want to delete the user? [y/n]: $ y
Deleting user: Camila
$ python main.py delete Wade
# Are you sure you want to delete the user? [y/n]: $ n
Operation cancelled
// And finally, the command delete-all
// Notice it doesn't have CLI arguments, only a CLI option
$ python main.py delete-all
# Are you sure you want to delete ALL users? [y/n]: $ y
Deleting all users
$ python main.py delete-all
# Are you sure you want to delete ALL users? [y/n]: $ n
Operation cancelled
// And if you pass the --force CLI option, it doesn't need to confirm
$ python main.py delete-all --force
Deleting all users
// And init that doesn't take any CLI parameter
$ python main.py init
Initializing user database
```
================================================
FILE: docs/tutorial/exceptions.md
================================================
# Exceptions and Errors
When your code has errors and you run it, it will show the error and an exception.
Typer does some tricks to help you detect those errors quickly.
## Example Broken App
Let's take this example broken app:
{* docs_src/exceptions/tutorial001_py310.py hl[8] *}
This code is broken because you can't sum a string and a number (`name + 3`).
## Exceptions with Rich
**Typer** will automatically use Rich to automatically show you nicely printed errors.
It will **omit** all the parts of the traceback (the chain of things that called your function) that come from the internal parts in Typer.
So, the error you see will be **much clearer** and simpler, to help you detect the problem in your code quickly:
```console
$ python main.py
╭──────────────── Traceback (most recent call last) ────────────────╮
│ /home/user/code/superapp/main.py:8 in main │
│ │
│ 5 │
│ 6 @app.command() │
│ 7 def main(name: str = "morty"): │
│ ❱ 8 │ print(name + 3) │
│ 9 │
│ 10 │
│ 11 if __name__ == "__main__": │
╰───────────────────────────────────────────────────────────────────╯
TypeError: can only concatenate str (not "int") to str
```
## Exceptions without Rich
You can disable Rich globally using the environmental variable `TYPER_USE_RICH`.
In this case, Typer will still do some tricks to show you the information **as clearly as possible**:
```console
$ python main.py
Traceback (most recent call last):
File "main.py", line 12, in
app()
File "main.py", line 8, in main
print(name + 3)
TypeError: can only concatenate str (not "int") to str
```
## Show Local Variables for Detailed Debugging
When using Rich, you can get more verbose output by printing the values of the local variables as part of the error message.
By default, this setting is disabled (since Typer 0.23.0) to avoid showing **delicate information**, for example a **password**, a **key** or a **token**.
In these cases, it could be problematic if the automatic errors show the value in those local variables.
This would be relevant in particular if your CLI application is being run on some CI (continuous integration) system that is recording the logs.
However, if you do want to enable the setting, you can set the parameter `pretty_exceptions_show_locals=True` when creating the `typer.Typer()` application:
{* docs_src/exceptions/tutorial002_py310.py hl[3] *}
Now, when using Rich, you will see the error with the local variables:
```console
$ python main.py
╭──────────────── Traceback (most recent call last) ────────────────╮
│ /home/user/code/superapp/main.py:5 in main │
│ │
│ 2 │
│ 3 │
│ 4 def main(name: str = "morty"): │
│ ❱ 5 │ print(name + 3) │
│ 6 │
│ 7 │
│ 8 if __name__ == "__main__": │
│ │
│ ╭──── locals ────╮ │
│ │ name = 'morty' │ │
│ ╰────────────────╯ │
╰───────────────────────────────────────────────────────────────────╯
TypeError: can only concatenate str (not "int") to str
```
Being able to see the values of local variables is very **helpful** to diagnose, **debug**, and fix problems.
But you should only enable it if you're not dealing with delicate information.
## Disable Short Output
If you want to show the full exception, including the internal parts in Typer, you can use the parameter `pretty_exceptions_short=False`:
{* docs_src/exceptions/tutorial003_py310.py hl[3] *}
Now when you run it, you will see the whole output:
```console
$ python main.py
╭──────────────── Traceback (most recent call last) ────────────────╮
│ /home/user/code/superapp/main.py:12 in <module> │
│ │
│ 9 │
│ 10 │
│ 11 if __name__ == "__main__": │
│ ❱ 12 │ app() │
│ 13 │
│ │
│ ╭─────────────────────────── locals ────────────────────────────╮ │
│ │ __annotations__ = {} │ │
│ │ __builtins__ = <module 'builtins' (built-in)> │ │
│ │ __cached__ = None │ │
│ │ __doc__ = None │ │
│ │ __file__ = 'main.py' │ │
│ │ __loader__ = <_frozen_importlib_external.SourceFileLoad… │ │
│ │ object at 0x7f047db1c050> │ │
│ │ __name__ = '__main__' │ │
│ │ __package__ = None │ │
│ │ __spec__ = None │ │
│ │ app = <typer.main.Typer object at 0x7f047db51d90> │ │
│ │ main = <function main at 0x7f047db56830> │ │
│ │ typer = <module 'typer' from │ │
│ │ '/home/user/code/superapp/env/lib/python3.… │ │
│ ╰───────────────────────────────────────────────────────────────╯ │
│ │
│ /home/user/code/superapp/env/lib/python3.7/site-packages/typer/ma │
│ in.py:328 in __call__ │
│ │
│ /home/user/code/superapp/env/lib/python3.7/site-packages/typer/ma │
│ in.py:311 in __call__ │
│ │
│ /home/user/code/superapp/env/lib/python3.7/site-packages/click/co │
│ re.py:1130 in __call__ │
│ │
│ /home/user/code/superapp/env/lib/python3.7/site-packages/typer/co │
│ re.py:723 in main │
│ │
│ /home/user/code/superapp/env/lib/python3.7/site-packages/typer/co │
│ re.py:216 in _main │
│ │
│ /home/user/code/superapp/env/lib/python3.7/site-packages/click/co │
│ re.py:1404 in invoke │
│ │
│ /home/user/code/superapp/env/lib/python3.7/site-packages/click/co │
│ re.py:760 in invoke │
│ │
│ /home/user/code/superapp/env/lib/python3.7/site-packages/typer/ma │
│ in.py:683 in wrapper │
│ │
│ /home/user/code/superapp/main.py:8 in main │
│ │
│ 5 │
│ 6 @app.command() │
│ 7 def main(name: str = "morty"): │
│ ❱ 8 │ print(name + 3) │
│ 9 │
│ 10 │
│ 11 if __name__ == "__main__": │
│ │
│ ╭──── locals ────╮ │
│ │ name = 'morty' │ │
│ ╰────────────────╯ │
╰───────────────────────────────────────────────────────────────────╯
TypeError: can only concatenate str (not "int") to str
```
## Disable Pretty Exceptions
You can also entirely disable pretty exceptions with the parameter `pretty_exceptions_enable=False`:
{* docs_src/exceptions/tutorial004_py310.py hl[3] *}
And now you will see the full standard exception as with any other Python program:
```console
$ python main.py
Traceback (most recent call last):
File "main.py", line 12, in
app()
File "/home/user/code/superapp/env/lib/python3.7/site-packages/typer/main.py", line 328, in __call__
raise e
File "/home/user/code/superapp/env/lib/python3.7/site-packages/typer/main.py", line 311, in __call__
return get_command(self)(*args, **kwargs)
File "/home/user/code/superapp/env/lib/python3.7/site-packages/click/core.py", line 1130, in __call__
return self.main(*args, **kwargs)
File "/home/user/code/superapp/env/lib/python3.7/site-packages/typer/core.py", line 723, in main
**extra,
File "/home/user/code/superapp/env/lib/python3.7/site-packages/typer/core.py", line 216, in _main
rv = self.invoke(ctx)
File "/home/user/code/superapp/env/lib/python3.7/site-packages/click/core.py", line 1404, in invoke
return ctx.invoke(self.callback, **ctx.params)
File "/home/user/code/superapp/env/lib/python3.7/site-packages/click/core.py", line 760, in invoke
return __callback(*args, **kwargs)
File "/home/user/code/superapp/env/lib/python3.7/site-packages/typer/main.py", line 683, in wrapper
return callback(**use_params) # type: ignore
File "main.py", line 8, in main
print(name + 3)
TypeError: can only concatenate str (not "int") to str
```
You could also achieve the same with the environment variable `TYPER_STANDARD_TRACEBACK=1` (or by setting the deprecated variable `_TYPER_STANDARD_TRACEBACK=1`).
This will work for any other Typer program too, in case you need to debug a problem in a Typer program made by someone else:
```console
export TYPER_STANDARD_TRACEBACK=1
$ python main.py
Traceback (most recent call last):
File "main.py", line 12, in
app()
File "/home/user/code/superapp/env/lib/python3.7/site-packages/typer/main.py", line 328, in __call__
raise e
File "/home/user/code/superapp/env/lib/python3.7/site-packages/typer/main.py", line 311, in __call__
return get_command(self)(*args, **kwargs)
File "/home/user/code/superapp/env/lib/python3.7/site-packages/click/core.py", line 1130, in __call__
return self.main(*args, **kwargs)
File "/home/user/code/superapp/env/lib/python3.7/site-packages/typer/core.py", line 723, in main
**extra,
File "/home/user/code/superapp/env/lib/python3.7/site-packages/typer/core.py", line 216, in _main
rv = self.invoke(ctx)
File "/home/user/code/superapp/env/lib/python3.7/site-packages/click/core.py", line 1404, in invoke
return ctx.invoke(self.callback, **ctx.params)
File "/home/user/code/superapp/env/lib/python3.7/site-packages/click/core.py", line 760, in invoke
return __callback(*args, **kwargs)
File "/home/user/code/superapp/env/lib/python3.7/site-packages/typer/main.py", line 683, in wrapper
return callback(**use_params) # type: ignore
File "main.py", line 8, in main
print(name + 3)
TypeError: can only concatenate str (not "int") to str
```
================================================
FILE: docs/tutorial/first-steps.md
================================================
# First Steps
## The simplest example
The simplest **Typer** file could look like this:
{* docs_src/first_steps/tutorial001_py310.py *}
Copy that to a file `main.py`.
Test it:
```console
$ python main.py
Hello World
// It just prints "Hello World".
// Now check the --help
$ python main.py --help
Usage: main.py [OPTIONS]
╭─ Options ─────────────────────────────────────────╮
│ --help Show this message │
│ and exit. │
╰───────────────────────────────────────────────────╯
```
...but this program is still not very useful. Let's extend it.
## What is a **CLI argument**
Here we will use the word **CLI argument** to refer to **CLI parameters** passed in some specific order to the CLI application. By default, they are *required*.
If you go to your terminal and type:
```bash
$ ls ./myproject
first-steps.md intro.md
```
`ls` will show the contents of the directory `./myproject`.
* `ls` is the *program* (or "command", "CLI app").
* `./myproject` is a *CLI argument*, in this case it refers to the path of a directory.
They are a bit different from **CLI options** that you will see later below.
## Add a CLI argument
Update the previous example with an argument `name`:
{* docs_src/first_steps/tutorial002_py310.py hl[4,5] *}
```console
$ python main.py
// If you run it without the argument, it shows a nice error
Usage: main.py [OPTIONS] NAME
Try 'main.py --help' for help.
╭─ Error ───────────────────────────────────────────╮
│ Missing argument 'NAME'. │
╰───────────────────────────────────────────────────╯
// Now pass that NAME CLI argument
$ python main.py Camila
Hello Camila
// Here "Camila" is the CLI argument
// To pass a name with spaces for the same CLI argument, use quotes
$ python main.py "Camila Gutiérrez"
Hello Camila Gutiérrez
```
/// tip
If you need to pass a single value that contains spaces to a *CLI argument*, use quotes (`"`) around it.
///
## Two CLI arguments
Now let's say we want to have the name and last name separated.
So, extend that to have 2 arguments, `name` and `lastname`:
{* docs_src/first_steps/tutorial003_py310.py hl[4,5] *}
```console
// Check the main --help
$ python main.py --help
Usage: main.py [OPTIONS] NAME
Try 'main.py --help' for help.
╭─ Error ───────────────────────────────────────────╮
│ Missing argument 'NAME'. │
╰───────────────────────────────────────────────────╯
typer on richify [»!?] via 🐍 v3.7.5 (env3.7)
❯ python main.py
Usage: main.py [OPTIONS] NAME LASTNAME
Try 'main.py --help' for help.
╭─ Error ───────────────────────────────────────────╮
│ Missing argument 'NAME'. │
╰───────────────────────────────────────────────────╯
// There are now 2 CLI arguments, name and lastname
// Now pass a single name argument
$ python main.py Camila
Usage: main.py [OPTIONS] NAME LASTNAME
Try 'main.py --help' for help.
╭─ Error ───────────────────────────────────────────╮
│ Missing argument 'LASTNAME'. │
╰───────────────────────────────────────────────────╯
// These 2 arguments are required, so, pass both:
$ python main.py Camila Gutiérrez
Hello Camila Gutiérrez
```
/// tip
Notice that the order is important. The last name has to go after the first name.
If you called it with:
```
$ python main.py Gutiérrez Camila
```
your app wouldn't have a way to know which is the `name` and which the `lastname`. It expects the first *CLI argument* to be the `name` and the second *CLI argument* to be the `lastname`.
///
## What is a **CLI option**
Here we will use the word **CLI option** to refer to *CLI parameters* passed to the CLI application with a specific name. For example, if you go to your terminal and type:
```console
$ ls ./myproject --size
12 first-steps.md 4 intro.md
```
`ls` will show the contents of the directory `./myproject` with their `size`.
* `ls` is the *program* (or "command", "CLI app").
* `./myproject` is a *CLI argument*.
* `--size` is an optional *CLI option*.
The program knows it has to show the size because it sees `--size`, not because of the order.
A *CLI option* like `--size` doesn't depend on the order like a *CLI argument*.
So, if you put the `--size` *before* the *CLI argument*, it still works (in fact, that's the most common way of doing it):
```console
$ ls --size ./myproject
12 first-steps.md 4 intro.md
```
The main visual difference between a *CLI option* and a *CLI argument* is that the *CLI option* has `--` prepended to the name, like in "`--size`".
A *CLI option* doesn't depend on the order because it has a predefined name (here it's `--size`). This is because the CLI app is looking specifically for a literal `--size` parameter (also known as "flag" or "switch"), with that specific "name" (here the specific name is "`--size`"). The CLI app will check if you typed it or not, it will be actively looking for `--size` even if you didn't type it (to check if it's there or not).
In contrast, the CLI app is not actively looking for the *CLI argument* with a text "`./myproject`", it has no way to know if you would type `./myproject` or `./my-super-awesome-project` or anything else. It's just waiting to get whatever you give it. The only way to know that you refer to a specific *CLI argument* is because of the order. The same way that it knows that the first *CLI argument* was the `name` and the second was the `lastname`, but if you mixed the order, it wouldn't be able to handle it.
Instead, with a *CLI option*, the order doesn't matter.
Also, by default, a *CLI option* is *optional* (not *required*).
So, by default:
* A *CLI argument* is **required**
* A *CLI option* is **optional**
But the *required* and *optional* defaults can be changed.
So, the main and **most important** difference is that:
* *CLI options* **start with `--`** and don't depend on the order
* *CLI arguments* depend on the **sequence order**
/// tip
In this example above the *CLI option* `--size` is just a "flag" or "switch" that will contain a boolean value, `True` or `False`, depending on if it was added to the command or not.
This one doesn't receive any values. But *CLI options* can also receive values like *CLI arguments*. You'll see how later.
///
## Add one *CLI option*
Now add a `--formal` *CLI option*:
{* docs_src/first_steps/tutorial004_py310.py hl[4,5] *}
Here `formal` is a `bool` that is `False` by default.
```console
// Get the help
$ python main.py --help
Usage: main.py [OPTIONS] NAME LASTNAME
╭─ Arguments ─────────────────────────────────────────────────────╮
│ * name TEXT [default: None] [required] │
│ * lastname TEXT [default: None] [required] │
╰─────────────────────────────────────────────────────────────────╯
╭─ Options ───────────────────────────────────────────────────────╮
│ --formal --no-formal [default: no-formal] │
│ --help Show this message and │
│ exit. │
╰─────────────────────────────────────────────────────────────────╯
```
/// tip
Notice that it automatically creates a `--formal` and a `--no-formal` because it detected that `formal` is a `bool`.
///
Now call it normally:
```console
$ python main.py Camila Gutiérrez
Hello Camila Gutiérrez
// But if you pass --formal
$ python main.py Camila Gutiérrez --formal
Good day Ms. Camila Gutiérrez.
// And as --formal is a CLI option you can put it anywhere in this command
$ python main.py Camila --formal Gutiérrez
Good day Ms. Camila Gutiérrez.
$ python main.py --formal Camila Gutiérrez
Good day Ms. Camila Gutiérrez.
```
## A *CLI option* with a value
To convert the `lastname` from a *CLI argument* to a *CLI option*, give it a default value of `""`:
{* docs_src/first_steps/tutorial005_py310.py hl[4] *}
As `lastname` now has a default value of `""` (an empty string) it is no longer required in the function, and **Typer** will now by default make it an optional *CLI option*.
```console
$ python main.py --help
Usage: main.py [OPTIONS] NAME
╭─ Arguments ───────────────────────────────────────────────────────╮
│ * name TEXT [default: None] [required] │
╰───────────────────────────────────────────────────────────────────╯
╭─ Options ─────────────────────────────────────────────────────────╮
│ --lastname TEXT │
│ --formal --no-formal [default: no-formal] │
│ --help Show this message │
│ and exit. │
╰───────────────────────────────────────────────────────────────────╯
```
/// tip
Notice the `--lastname`, and notice that it takes a textual value.
A *CLI option* with a value like `--lastname` (contrary to a *CLI option* without a value, a `bool` flag, like `--formal` or `--size`) takes as its value whatever is at the *right side* of the *CLI option*.
///
```console
// Call it without a --lastname
$ python main.py Camila
Hello Camila
// Pass the --lastname
$ python main.py Camila --lastname Gutiérrez
Hello Camila Gutiérrez
```
/// tip
Notice that "`Gutiérrez`" is at the right side of `--lastname`. A *CLI option* with a value takes as its value whatever is at the *right side*.
///
And as `--lastname` is now a *CLI option* that doesn't depend on the order, you can pass it before the name:
```console
$ python main.py --lastname Gutiérrez Camila
// and it will still work normally
Hello Camila Gutiérrez
```
## Document your CLI app
If you add a docstring to your function it will be used in the help text:
{* docs_src/first_steps/tutorial006_py310.py hl[5,6,7,8,9] *}
Now see it with the `--help` option:
```console
$ python main.py --help
Usage: main.py [OPTIONS] NAME
Say hi to NAME, optionally with a --lastname.
If --formal is used, say hi very formally.
╭─ Arguments ───────────────────────────────────────────────────────╮
│ * name TEXT [default: None] [required] │
╰───────────────────────────────────────────────────────────────────╯
╭─ Options ─────────────────────────────────────────────────────────╮
│ --lastname TEXT │
│ --formal --no-formal [default: no-formal] │
│ --help Show this message │
│ and exit. │
╰───────────────────────────────────────────────────────────────────╯
```
/// tip
There is another place to document the specific *CLI options* and *CLI arguments* that will show up next to them in the help text as with `--install-completion` or `--help`, you will learn that later in the tutorial.
///
## Arguments, options, parameters, optional, required
Be aware that these terms refer to multiple things depending on the context, and sadly, those "contexts" mix frequently, so it's easy to get confused.
### In Python
In Python, the names of the variables in a function, like `name` and `lastname`:
```Python
def main(name: str, lastname: str = ""):
pass
```
are called "Python function parameters" or "Python function arguments".
/// note | Technical Details
There's actually a very small distinction in Python between "parameter" and "argument".
It's quite technical... and somewhat pedantic.
*Parameter* refers to the variable name in a function *declaration*. Like:
```
def bring_person(name: str, lastname: str = ""):
pass
```
*Argument* refers to the value passed when *calling* a function. Like:
```
person = bring_person("Camila", lastname="Gutiérrez")
```
...but you will probably see them used interchangeably in most of the places (including here).
///
#### Python default values
In Python, in a function, a parameter with a *default value* like `lastname` in:
```Python
def main(name: str, lastname: str = ""):
pass
```
is considered an "optional parameter" (or "optional argument").
The default value can be anything, like `""` or `None`.
And a parameter like `name`, that doesn't have a default value, is considered *required*.
### In CLIs
When talking about command line interface applications, the words **"argument"** and **"parameter"** are commonly used to refer to that data passed to a CLI app, those parameters.
But those words **don't imply** anything about the data being required, needing to be passed in a certain order, nor having a flag like `--lastname`.
The parameters that come with a name like `--lastname` (and optionally a value) are commonly optional, not required. So, when talking about CLIs it's common to call them **optional arguments** or **optional parameters**. Sometimes these *optional parameters* that start with `--` are also called a **flag** or a **switch**.
In reality, the parameters that require an order can be made *optional* too. And the ones that come with a flag (like `--lastname`) can be *required* too.
### In **Typer**
To try and make it a bit easier, we'll normally use the words "parameter" or "argument" to refer to "Python functions parameters" or "Python functions arguments".
We'll use ***CLI argument*** to refer to those *CLI parameters* that depend on the specific order. That are **required** by default.
And we'll use ***CLI option*** to refer to those *CLI parameters* that depend on a name that starts with `--` (like `--lastname`). That are **optional** by default.
We will use ***CLI parameter*** to refer to both, *CLI arguments* and *CLI options*.
## The `typer` Command
When you install `typer`, by default it adds a `typer` command to your shell.
This `typer` command allows you to run your scripts with ✨ auto completion ✨ in your terminal.
As an alternative to running with Python:
```console
$ python main.py
Hello World
```
You can run with the `typer` command:
```console
$ typer main.py run
Hello World
```
...and it will give you auto completion in your terminal when you hit TAB for all your code.
So you can use it to have auto completion for your own scripts as you continue with the tutorial.
/// tip
Your CLI application built with **Typer** won't need the `typer` command to have auto completion once you create a Python package.
But for short scripts and for learning, before creating a Python package, it might be useful.
///
================================================
FILE: docs/tutorial/index.md
================================================
# Learn
Learn how to use **Typer** in this step-by-step **Tutorial** - **User Guide**.
It covers everything you need to know from the **simplest scripts** to **complex CLI applications**.
You could consider this a **book**, a **course**, the **official** and recommended way to learn **Typer**. 😎
## Python Types
If you need a refresher about how to use Python type hints, check the first part of FastAPI's Python types intro.
You can also check the mypy cheat sheet.
In short (very short), you can declare a function with parameters like:
```Python
from typing import Optional
def type_example(name: str, formal: bool = False, intro: Optional[str] = None):
pass
```
And your editor (and **Typer**) will know that:
* `name` is of type `str` and is a required parameter.
* `formal` is a `bool` and is by default `False`.
* `intro` is an optional `str`, by default is `None`.
These type hints are what give you autocomplete in your editor and several other features.
**Typer** is based on these type hints.
## About this Tutorial
This tutorial shows you how to use **Typer** with all its features, step by step.
Each section gradually builds on the previous ones, but it's structured to separate topics, so that you can go directly to any specific one to solve your specific CLI needs.
It is also built to work as a future reference so you can come back and see exactly what you need.
## Run the Code
All the code blocks can be copied and used directly (they are tested Python files).
To run any of the examples, copy the code to a file `main.py`, and run it:
```console
$ python main.py
✨ The magic happens here ✨
```
It is **HIGHLY encouraged** that you write or copy the code, edit it and run it locally.
Using it in your editor is what really shows you the benefits of **Typer**, seeing how little code you have to write, all the **inline errors**, **autocompletion**, etc.
And running the examples is what will really help you **understand** what is going on.
You can learn a lot more by **running some examples** and **playing around** with them than by reading all the docs here.
================================================
FILE: docs/tutorial/install.md
================================================
# Install **Typer**
The first step is to install **Typer**.
First, make sure you create your [virtual environment](../virtual-environments.md){.internal-link target=_blank}, activate it, and then install it, for example with:
```console
$ pip install typer
---> 100%
Successfully installed typer click shellingham rich
```
By default, `typer` comes with `rich` and `shellingham`.
================================================
FILE: docs/tutorial/launch.md
================================================
# Launching Applications
You can launch applications from your CLI program with `typer.launch()`.
It will launch the appropriate application depending on the URL or file type you pass it:
{* docs_src/launch/tutorial001_py310.py hl[9] *}
Check it:
```console
$ python main.py
Opening Typer docs
// Opens browser with Typer's docs
```
## Locating a file
You can also make the operating system open the file browser indicating where a file is located with `locate=True`:
{* docs_src/launch/tutorial002_py310.py hl[20] *}
/// tip
The rest of the code in this example is just making sure the app directory exists and creating the config file.
But the most important part is the `typer.launch(config_file_str, locate=True)` with the argument `locate=True`.
///
Check it:
```console
$ python main.py
Opening config directory
// Opens a file browser indicating where the config file is located
```
================================================
FILE: docs/tutorial/multiple-values/arguments-with-multiple-values.md
================================================
# CLI Arguments with Multiple Values
*CLI arguments* can also receive multiple values.
You can define the type of a *CLI argument* using `list`.
{* docs_src/multiple_values/arguments_with_multiple_values/tutorial001_py310.py hl[9] *}
And then you can pass it as many *CLI arguments* of that type as you want:
```console
$ python main.py ./index.md ./first-steps.md woohoo!
This file exists: index.md
woohoo!
This file exists: first-steps.md
woohoo!
```
/// tip
We also declared a final *CLI argument* `celebration`, and it's correctly used even if we pass an arbitrary number of `files` first.
///
/// info
A `list` can only be used in the last command (if there are subcommands), as this will take anything to the right and assume it's part of the expected *CLI arguments*.
///
## *CLI arguments* with tuples
If you want a specific number of values and types, you can use a tuple, and it can even have default values:
{* docs_src/multiple_values/arguments_with_multiple_values/tutorial002_an_py310.py hl[10:12] *}
Check it:
```console
// Check the help
$ python main.py --help
Usage: main.py [OPTIONS] [NAMES]...
Arguments:
[NAMES]... Select 3 characters to play with [default: Harry, Hermione, Ron]
Options:
--help Show this message and exit.
// Use it with its defaults
$ python main.py
Hello Harry
Hello Hermione
Hello Ron
// If you pass an invalid number of arguments you will get an error
$ python main.py Draco Hagrid
Error: Argument 'names' takes 3 values
// And if you pass the exact number of values it will work correctly
$ python main.py Draco Hagrid Dobby
Hello Draco
Hello Hagrid
Hello Dobby
```
================================================
FILE: docs/tutorial/multiple-values/index.md
================================================
# Multiple Values
There are several ways to declare multiple values for *CLI options* and *CLI arguments*.
We'll see them in the next short sections.
================================================
FILE: docs/tutorial/multiple-values/multiple-options.md
================================================
# Multiple CLI Options
You can declare a *CLI option* that can be used multiple times, and then get all the values.
For example, let's say you want to accept several users in a single execution.
For this, use the standard Python `list` to declare it as a list of `str`:
{* docs_src/multiple_values/multiple_options/tutorial001_an_py310.py hl[9] *}
You will receive the values as you declared them, as a `list` of `str`.
Check it:
```console
// The default value is 'None'
$ python main.py
No provided users (raw input = None)
Aborted!
// Now pass a user
$ python main.py --user Camila
Processing user: Camila
// And now try with several users
$ python main.py --user Camila --user Rick --user Morty
Processing user: Camila
Processing user: Rick
Processing user: Morty
```
## Multiple `float`
The same way, you can use other types and they will be converted by **Typer** to their declared type:
{* docs_src/multiple_values/multiple_options/tutorial002_an_py310.py hl[9] *}
Check it:
```console
$ python main.py
The sum is 0
// Try with some numbers
$ python main.py --number 2
The sum is 2.0
// Try with some numbers
$ python main.py --number 2 --number 3 --number 4.5
The sum is 9.5
```
================================================
FILE: docs/tutorial/multiple-values/options-with-multiple-values.md
================================================
# CLI Options with Multiple Values
You can also declare a *CLI option* that takes several values of different types.
You can set the number of values and types to anything you want, but it has to be a fixed number of values.
For this, use the standard Python `tuple`:
{* docs_src/multiple_values/options_with_multiple_values/tutorial001_an_py310.py hl[9] *}
Each of the internal types defines the type of each value in the tuple.
So:
```Python
user: tuple[str, int, bool]
```
means that the parameter `user` is a tuple of 3 values.
* The first value is a `str`.
* The second value is an `int`.
* The third value is a `bool`.
Later we do:
```Python
username, coins, is_wizard = user
```
If you hadn't seen that, it means that `user` is a tuple with 3 values, and we are assigning each of the values to a new variable:
* The first value in the tuple `user` (a `str`) goes to the variable `username`.
* The second value in the tuple `user` (an `int`) goes to the variable `coins`.
* The third value in the tuple `user` (a `bool`) goes to the variable `is_wizard`.
So, this:
```Python
username, coins, is_wizard = user
```
is equivalent to this:
```Python
username = user[0]
coins = user[1]
is_wizard = user[2]
```
## Check it
Now let's see how this works in the terminal:
```console
// check the help
$ python main.py --help
// Notice the <TEXT INTEGER BOOLEAN>
Usage: main.py [OPTIONS]
Options:
--user <TEXT INTEGER BOOLEAN>...
--help Show this message and exit.
// Now try it
$ python main.py --user Camila 50 yes
The username Camila has 50 coins
And this user is a wizard!
// With other values
$ python main.py --user Morty 3 no
The username Morty has 3 coins
// Try with invalid values (not enough)
$ python main.py --user Camila 50
Error: Option '--user' requires 3 arguments
```
================================================
FILE: docs/tutorial/one-file-per-command.md
================================================
# One File Per Command
When your CLI application grows, you can split it into multiple files and modules. This pattern helps maintain a clean and organized code structure. ✨
This tutorial will show you how to use `add_typer` to create sub commands and organize your commands in multiple files.
We will create a simple CLI with the following commands:
- `version`
- `users add NAME`
- `users delete NAME`
## CLI structure
Here is the structure we'll be working with:
```text
mycli/
├── __init__.py
├── main.py
├── users/
│ ├── __init__.py
│ ├── add.py
│ └── delete.py
└── version.py
```
`mycli` will be our package, and it will contain the following modules:
- `main.py`: The main module that will import the `version` and `users` modules.
- `version.py`: A module that will contain the `version` command.
- `users/`: A package (inside of our `mycli` package) that will contain the `add` and `delete` commands.
## Implementation
Let's start implementing our CLI! 🚀
We'll create the `version` module, the `main` module, and the `users` package.
### Version Module (`version.py`)
Let's start by creating the `version` module. This module will contain the `version` command.
{* docs_src/one_file_per_command/app_py310/version.py *}
In this file we are creating a new Typer app instance for the `version` command.
This is not required in single-file applications, but in the case of multi-file applications it will allow us to include this command in the main application using `app.add_typer()`.
Let's see that next!
### Main Module (`main.py`)
The main module will be the entry point of the application. It will import the version module and the users module.
/// tip
We'll see how to implement the users module in the next section.
///
{* docs_src/one_file_per_command/app_py310/main.py hl[8,9] *}
In this module, we import the `version` and `users` modules and add them to the main app using `app.add_typer()`.
For the `users` module, we specify the name as `"users"` to group the commands under the `users` sub-command.
Notice that we didn't add a name for the `version_app` Typer app. Because of this, Typer will add the commands (just one in this case) declared in the `version_app` directly at the top level. So, there will be a top-level `version` sub-command.
But for `users`, we add a name `"users"`, this way those commands will be under the sub-command `users` instead of at the top level. So, there will be a `users add` and `users delete` sub-sub-commands. 😅
/// tip
If you want a command to group the included commands in a sub-app, add a name.
If you want to include the commands from a sub-app directly at the top level, don't add a name, or set it to `None`. 🤓
///
Let's now create the `users` module with the `add` and `delete` commands.
### Users Add Command (`users/add.py`)
{* docs_src/one_file_per_command/app_py310/users/add.py *}
Like the `version` module, we create a new Typer app instance for the `users/add` command. This allows us to include the `add` command in the users app.
### Users Delete Command (`users/delete.py`)
{* docs_src/one_file_per_command/app_py310/users/delete.py *}
And once again, we create a new Typer app instance for the `users/delete` command. This allows us to include the `delete` command in the users app.
### Users' app (`users/__init__.py`)
Finally, we need to create an `__init__.py` file in the `users` directory to define the `users` app.
{* docs_src/one_file_per_command/app_py310/users/__init__.py *}
Similarly to the `version` module, we create a new `Typer` app instance for the `users` module. This allows us to include the `add` and `delete` commands in the users app.
## Running the Application
Now we are ready to run the application! 😎
To run the application, you can execute it as a Python module:
```console
$ python -m mycli.main version
My CLI Version 1.0
$ python -m mycli.main users add Camila
Adding user: Camila
```
And if you built a package and installed your app, you can then use the `mycli` command:
```console
$ mycli version
My CLI Version 1.0
$ mycli users add Camila
Adding user: Camila
```
## Callbacks
Have in mind that if you include a sub-app with `app.add_typer()` **without a name**, the commands will be added to the top level, so **only the top level callback** (if there's any) will be used, the one declared in the main app.
If you **want to use a callback** for a sub-app, you need to include the sub-app **with a name**, which creates a sub-command grouping the commands in that sub-app. 🤓
In the example above, if the `users` sub-app had a callback, it would be used. But if the `version` sub-app had a callback, it would not be used, because the `version` sub-app was included without a name.
================================================
FILE: docs/tutorial/options/callback-and-context.md
================================================
# CLI Option Callback and Context
In some occasions you might want to have some custom logic for a specific *CLI parameter* (for a *CLI option* or *CLI argument*) that is executed with the value received from the terminal.
In those cases you can use a *CLI parameter* callback function.
## Validate *CLI parameters*
For example, you could do some validation before the rest of the code is executed.
{* docs_src/options/callback/tutorial001_an_py310.py hl[8:11,15] *}
Here you pass a function to `typer.Option()` or `typer.Argument()` with the keyword argument `callback`.
The function receives the value from the command line. It can do anything with it, and then return the value.
In this case, if the `--name` is not `Camila` we raise a `typer.BadParameter()` exception.
The `BadParameter` exception is special, it shows the error with the parameter that generated it.
Check it:
```console
$ python main.py --name Camila
Hello Camila
$ python main.py --name Rick
Usage: main.py [OPTIONS]
// We get the error from the callback
Error: Invalid value for '--name': Only Camila is allowed
```
## Handling completion
There's something to be aware of with callbacks and completion that requires some small special handling.
But first let's just use completion in your shell (Bash, Zsh, Fish, or PowerShell).
After installing completion (for your own Python package), when you use your CLI program and start adding a *CLI option* with `--` and then hit TAB, your shell will show you the available *CLI options* (the same for *CLI arguments*, etc).
To check it quickly with the previous script use the `typer` command:
```console
// Hit the TAB key in your keyboard below where you see the: [TAB]
$ typer ./main.py [TAB][TAB]
// Depending on your terminal/shell you will get some completion like this ✨
run -- Run the provided Typer app.
utils -- Extra utility commands for Typer apps.
// Then try with "run" and --help
$ typer ./main.py run --help
// You get a help text with your CLI options as you normally would
Usage: typer run [OPTIONS]
Run the provided Typer app.
Options:
--name TEXT [required]
--help Show this message and exit.
// Then try completion with your program
$ typer ./main.py run --[TAB][TAB]
// You get completion for CLI options
--help -- Show this message and exit.
--name
// And you can run it as if it was with Python directly
$ typer ./main.py run --name Camila
Hello Camila
```
### How shell completion works
The way it works internally is that the shell/terminal will call your CLI program with some special environment variables (that hold the current *CLI parameters*, etc) and your CLI program will print some special values that the shell will use to present completion. All this is handled for you by **Typer** behind the scenes.
But the main **important point** is that it is all based on values printed by your program that the shell reads.
### Breaking completion in a callback
Let's say that when the callback is running, we want to show a message saying that it's validating the name:
{* docs_src/options/callback/tutorial002_an_py310.py hl[9] *}
And because the callback will be called when the shell calls your program asking for completion, that message `"Validating name"` will be printed and it will break completion.
It will look something like:
```console
// Run it normally
$ typer ./main.py run --name Camila
// See the extra message "Validating name"
Validating name
Hello Camila
$ typer ./main.py run --[TAB][TAB]
// Some weird broken error message ⛔️
(eval):1: command not found: Validating
rutyper ./main.pyed Typer app.
```
### Fix completion - using the `Context`
Every Typer application has a special object called a "Context" that is normally hidden.
But you can access the context by declaring a function parameter of type `typer.Context`.
The "context" has some additional data about the current execution of your program:
{* docs_src/options/callback/tutorial003_an_py310.py hl[8:10] *}
The `ctx.resilient_parsing` will be `True` when handling completion, so you can just return without printing anything else.
But it will be `False` when calling the program normally. So you can continue the execution of your previous code.
That's all is needed to fix completion. 🚀
Check it:
```console
$ typer ./main.py run --[TAB][TAB]
// Now it works correctly 🎉
--help -- Show this message and exit.
--name
// And you can call it normally
$ typer ./main.py run --name Camila
Validating name
Hello Camila
```
## Using the `CallbackParam` object
The same way you can access the `typer.Context` by declaring a function parameter with its value, you can declare another function parameter with type `typer.CallbackParam` to get the specific `Parameter` object.
{* docs_src/options/callback/tutorial004_an_py310.py hl[8,11] *}
It's probably not very common, but you could do it if you need it.
For example if you had a callback that could be used by several *CLI parameters*, that way the callback could know which parameter is each time.
Check it:
```console
$ python main.py --name Camila
Validating param: name
Hello Camila
```
## Technical Details
Because you get the relevant data in the callback function based on standard Python type annotations, you get type checks and autocompletion in your editor for free.
And **Typer** will make sure you get the function parameters you want.
You don't have to worry about their names, their order, etc.
As it's based on standard Python types, it "**just works**". ✨
### Callback with type annotations
You can get the `typer.Context` and the `typer.CallbackParam` simply by declaring a function parameter of each type.
The order doesn't matter, the name of the function parameters doesn't matter.
You could also get only the `typer.CallbackParam` and not the `typer.Context`, or vice versa, it will still work.
### `value` function parameter
The `value` function parameter in the callback can also have any name (e.g. `lastname`) and any type, but it should have the same type annotation as in the main function, because that's what it will receive.
It's also possible to not declare its type. It will still work.
And it's possible to not declare the `value` parameter at all, and, for example, only get the `typer.Context`. That will also work.
================================================
FILE: docs/tutorial/options/help.md
================================================
# CLI Options with Help
You already saw how to add a help text for *CLI arguments* with the `help` parameter.
Let's now do the same for *CLI options*:
{* docs_src/options/help/tutorial001_an_py310.py hl[11:12] *}
The same way as with `typer.Argument()`, we can put `typer.Option()` inside of `Annotated`.
We can then pass the `help` keyword parameter:
```Python
lastname: Annotated[str, typer.Option(help="this option does this and that")] = ""
```
...to create the help for that *CLI option*.
The same way as with `typer.Argument()`, **Typer** also supports the old style using the function parameter default value:
```Python
lastname: str = typer.Option(default="", help="this option does this and that")
```
Copy that example from above to a file `main.py`.
Test it:
```console
$ python main.py --help
Usage: main.py [OPTIONS] NAME
Say hi to NAME, optionally with a --lastname.
If --formal is used, say hi very formally.
Arguments:
NAME [required]
Options:
--lastname TEXT Last name of person to greet. [default: ]
--formal / --no-formal Say hi formally. [default: False]
--help Show this message and exit.
// Now you have a help text for the --lastname and --formal CLI options 🎉
```
## *CLI Options* help panels
The same as with *CLI arguments*, you can put the help for some *CLI options* in different panels to be shown with the `--help` option.
Using Rich, you can set the `rich_help_panel` parameter to the name of the panel you want for each *CLI option*:
{* docs_src/options/help/tutorial002_an_py310.py hl[15,21] *}
Now, when you check the `--help` option, you will see a default panel named "`Options`" for the *CLI options* that don't have a custom `rich_help_panel`.
And below you will see other panels for the *CLI options* that have a custom panel set in the `rich_help_panel` parameter:
```console
$ python main.py --help
Usage: main.py [OPTIONS] NAME
Say hi to NAME, optionally with a --lastname.
If --formal is used, say hi very formally.
╭─ Arguments ───────────────────────────────────────────────────────╮
│ * name TEXT [default: None] [required] │
╰───────────────────────────────────────────────────────────────────╯
╭─ Options ─────────────────────────────────────────────────────────╮
│ --lastname TEXT Last name of person to greet. │
│ --help Show this message and exit. │
╰───────────────────────────────────────────────────────────────────╯
╭─ Customization and Utils ─────────────────────────────────────────╮
│ --formal --no-formal Say hi formally. │
│ [default: no-formal] │
│ --debug --no-debug Enable debugging. │
│ [default: no-debug] │
╰───────────────────────────────────────────────────────────────────╯
```
Here we have a custom *CLI options* panel named "`Customization and Utils`".
## Help with style using Rich
In a future section you will see how to use custom markup in the `help` for *CLI options* when reading about [Commands - Command Help](../commands/help.md#rich-markdown-and-markup){.internal-link target=_blank}.
If you are in a hurry you can jump there, but otherwise, it would be better to continue reading here and following the tutorial in order.
## Hide default from help
You can tell Typer to not show the default value in the help text with `show_default=False`:
{* docs_src/options/help/tutorial003_an_py310.py hl[9] *}
And it will no longer show the default value in the help text:
```console
$ python main.py
Hello Wade Wilson
// Show the help
$ python main.py --help
Usage: main.py [OPTIONS]
Options:
--fullname TEXT
--help Show this message and exit.
// Notice there's no [default: Wade Wilson] 🔥
```
## Custom default string
You can use the same `show_default` to pass a custom string (instead of a `bool`) to customize the default value to be shown in the help text:
{* docs_src/options/help/tutorial004_an_py310.py hl[11] *}
And it will be used in the help text:
```console
$ python main.py
Hello Wade Wilson
// Show the help
$ python main.py --help
Usage: main.py [OPTIONS]
Options:
--fullname TEXT [default: (Deadpoolio the amazing's name)]
--help Show this message and exit.
// Notice how it shows "(Deadpoolio the amazing's name)" instead of the actual default of "Wade Wilson"
```
================================================
FILE: docs/tutorial/options/index.md
================================================
# CLI Options
In the next short sections we will see how to modify *CLI options* using `typer.Option()`.
`typer.Option()` works very similarly to `typer.Argument()`, but has some extra features that we'll see next.
================================================
FILE: docs/tutorial/options/name.md
================================================
# CLI Option Name
By default **Typer** will create a *CLI option* name from the function parameter.
So, if you have a function with:
```Python
def main(user_name: Optional[str] = None):
pass
```
or
```Python
def main(user_name: Annotated[Optional[str], typer.Option()] = None):
pass
```
**Typer** will create a *CLI option*:
```
--user-name
```
But you can customize it if you want to.
Let's say the function parameter name is `user_name` as above, but you want the *CLI option* to be just `--name`.
You can pass the *CLI option* name that you want to have in the following positional argument passed to `typer.Option()`:
{* docs_src/options/name/tutorial001_an_py310.py hl[9] *}
/// info
"Positional" means that it's not a function argument with a keyword name.
For example `show_default=True` is a keyword argument. "`show_default`" is the keyword.
But in `"--name"` there's no `option_name="--name"` or something similar, it's just the string value `"--name"` that goes in `typer.Option()`.
That's a "positional argument" in a function.
///
Check it:
```console
$ python main.py --help
// Notice the --name instead of --user-name
Usage: main.py [OPTIONS]
Options:
--name TEXT [required]
--help Show this message and exit.
// Try it
$ python main.py --name Camila
Hello Camila
```
## *CLI option* short names
A short name is a *CLI option* name with a single dash (`-`) instead of 2 (`--`) and a single letter, like `-n` instead of `--name`.
For example, the `ls` program has a *CLI option* named `--size`, and the same *CLI option* also has a short name `-s`:
```console
// With the long name --size
$ ls ./myproject --size
12 first-steps.md 4 intro.md
// With the short name -s
$ ls ./myproject -s
12 first-steps.md 4 intro.md
// Both CLI option names do the same
```
### *CLI option* short names together
Short names have another feature, when they have a single letter, as in `-s`, you can put several of these *CLI options* together, with a single dash.
For example, the `ls` program has these 2 *CLI options* (among others):
* `--size`: show the sizes of the listed files.
* `--human`: show a human-readable format, like `1MB` instead of just `1024`.
And these 2 *CLI options* have short versions too:
* `--size`: short version `-s`.
* `--human`: short version `-h`.
So, you can put them together with `-sh` or `-hs`:
```console
// Call ls with long CLI options
$ ls --size --human
12K first-steps.md 4.0K intro.md
// Now with short versions
$ ls -s -h
12K first-steps.md 4.0K intro.md
// And with short versions together
$ ls -sh
12K first-steps.md 4.0K intro.md
// Order in short versions doesn't matter
$ ls -hs
12K first-steps.md 4.0K intro.md
// They all work the same 🎉
```
### *CLI option* short names with values
When you use *CLI options* with short names, you can put them together if they are just boolean flags, like `--size` or `--human`.
But if you have a *CLI option* `--file` with a short name `-f` that takes a value, if you put it with other short names for *CLI options*, you have to put it as the last letter, so that it can receive the value that comes right after.
For example, let's say you are decompressing/extracting a file `myproject.tar.gz` with the program `tar`.
You can pass these *CLI option* short names to `tar`:
* `-x`: means "e`X`tract", to decompress and extract the contents.
* `-v`: means "`V`erbose", to print on the screen what it is doing, so you can know that it's decompressing each file and can entertain yourself while you wait.
* `-f`: means "`F`ile", this one requires a value, the compressed file to extract (in our example, this is `myproject.tar.gz`).
* So if you use all the short names together, this `-f` has to come last, to receive the value that comes next to it.
For example:
```console
$ tar -xvf myproject.tar.gz
myproject/
myproject/first-steps.md
myproject/intro.md
// But if you put the -f before
$ tar -fxv myproject.tar.gz
// You get an ugly error
tar: You must specify one of the blah, blah, error, error
```
### Defining *CLI option* short names
In **Typer** you can also define *CLI option* short names the same way you can customize the long names.
You can pass *positional* arguments to `typer.Option()` to define the *CLI option* name(s).
/// tip
Remember the *positional* function arguments are those that don't have a keyword.
All the other function arguments/parameters you pass to `typer.Option()` like `prompt=True` and `help="This option blah, blah"` require the keyword.
///
You can overwrite the *CLI option* name to use as in the previous example, but you can also declare extra alternatives, including short names.
For example, extending the previous example, let's add a *CLI option* short name `-n`:
{* docs_src/options/name/tutorial002_an_py310.py hl[9] *}
Here we are overwriting the *CLI option* name that by default would be `--user-name`, and we are defining it to be `--name`. And we are also declaring a *CLI option* short name of `-n`.
Check it:
```console
// Check the help
$ python main.py --help
// Notice the two CLI option names -n and --name
Usage: main.py [OPTIONS]
Options:
-n, --name TEXT [required]
--help Show this message and exit.
// Try the short version
$ python main.py -n Camila
Hello Camila
```
### *CLI option* only short name
If you only declare a short name like `-n` then that will be the only *CLI option* name. And neither `--name` nor `--user-name` will be available.
{* docs_src/options/name/tutorial003_an_py310.py hl[9] *}
Check it:
```console
$ python main.py --help
// Notice there's no --name nor --user-name, only -n
Usage: main.py [OPTIONS]
Options:
-n TEXT [required]
--help Show this message and exit.
// Try it
$ python main.py -n Camila
Hello Camila
```
### *CLI option* short name and default
Continuing with the example above, as **Typer** allows you to declare a *CLI option* as having only a short name, if you want to have the default long name plus a short name, you have to declare both explicitly:
{* docs_src/options/name/tutorial004_an_py310.py hl[9] *}
Check it:
```console
$ python main.py --help
// Notice that we have the long version --user-name back
// and we also have the short version -n
Usage: main.py [OPTIONS]
Options:
-n, --user-name TEXT [required]
--help Show this message and exit.
// Try it
$ python main.py --user-name Camila
Hello Camila
// And try the short version
$ python main.py -n Camila
```
### *CLI option* short names together
You can create multiple short names and use them together.
You don't have to do anything special for it to work (apart from declaring those short versions):
{* docs_src/options/name/tutorial005_an_py310.py hl[10:11] *}
/// tip
Notice that, again, we are declaring the long and short version of the *CLI option* names.
///
Check it:
```console
$ python main.py --help
// We now have short versions -n and -f
// And also long versions --name and --formal
Usage: main.py [OPTIONS]
Options:
-n, --name TEXT [required]
-f, --formal
--help Show this message and exit.
// Try the short versions
$ python main.py -n Camila -f
Good day Ms. Camila.
// And try the 2 short versions together
// See how -n has to go last, to be able to get the value
$ python main.py -fn Camila
Good day Ms. Camila.
```
================================================
FILE: docs/tutorial/options/password.md
================================================
# Password CLI Option and Confirmation Prompt
Apart from having a prompt, you can make a *CLI option* have a `confirmation_prompt=True`:
{* docs_src/options/password/tutorial001_an_py310.py hl[11] *}
And the CLI program will ask for confirmation:
```console
$ python main.py Camila
// It prompts for the email
# Email: $ camila@example.com
# Repeat for confirmation: $ camila@example.com
Hello Camila, your email is camila@example.com
```
## A Password prompt
When receiving a password, it is very common (in most shells) to not show anything on the screen while typing the password.
The program will still receive the password, but nothing will be shown on screen, not even `****`.
You can achieve the same using `hide_input=True`.
And if you combine it with `confirmation_prompt=True` you can easily receive a password with double confirmation:
{* docs_src/options/password/tutorial002_an_py310.py hl[12] *}
Check it:
```console
$ python main.py Camila
// It prompts for the password, but doesn't show anything when you type
# Password: $
# Repeat for confirmation: $
// Let's imagine the password typed was "typerrocks"
Hello Camila. Doing something very secure with password.
...just kidding, here it is, very insecure: typerrocks
```
================================================
FILE: docs/tutorial/options/prompt.md
================================================
# CLI Option Prompt
It's also possible to, instead of just showing an error, ask for the missing value with `prompt=True`:
{* docs_src/options/prompt/tutorial001_an_py310.py hl[9] *}
And then your program will ask the user for it in the terminal:
```console
// Call it with the NAME CLI argument
$ python main.py Camila
// It asks for the missing CLI option --lastname
# Lastname: $ Gutiérrez
Hello Camila Gutiérrez
```
## Customize the prompt
You can also set a custom prompt, passing the string that you want to use instead of just `True`:
{* docs_src/options/prompt/tutorial002_an_py310.py hl[11] *}
And then your program will ask for it using with your custom prompt:
```console
// Call it with the NAME CLI argument
$ python main.py Camila
// It uses the custom prompt
# Please tell me your last name: $ Gutiérrez
Hello Camila Gutiérrez
```
## Confirmation prompt
In some cases you could want to prompt for something and then ask the user to confirm it by typing it twice.
You can do it passing the parameter `confirmation_prompt=True`.
Let's say it's a CLI app to delete a project:
{* docs_src/options/prompt/tutorial003_an_py310.py hl[10] *}
And it will prompt the user for a value and then for the confirmation:
```console
$ python main.py
// Your app will first prompt for the project name, and then for the confirmation
# Project name: $ Old Project
# Repeat for confirmation: $ Old Project
Deleting project Old Project
// If the user doesn't type the same, receives an error and a new prompt
$ python main.py
# Project name: $ Old Project
# Repeat for confirmation: $ New Spice
Error: The two entered values do not match
# Project name: $ Old Project
# Repeat for confirmation: $ Old Project
Deleting project Old Project
// Now it works 🎉
```
================================================
FILE: docs/tutorial/options/required.md
================================================
# Required CLI Options
We said before that *by default*:
* *CLI options* are **optional**
* *CLI arguments* are **required**
Well, that's how they work *by default*, and that's the convention in many CLI programs and systems.
But if you really want, you can change that.
To make a *CLI option* required, you can put `typer.Option()` inside of `Annotated` and leave the parameter without a default value.
Let's make `--lastname` a required *CLI option*:
{* docs_src/options/required/tutorial001_an_py310.py hl[9] *}
The same way as with `typer.Argument()`, the old style of using the function parameter default value is also supported, in that case you would just not pass anything to the `default` parameter.
{* docs_src/options/required/tutorial001_py310.py hl[7] *}
Or you can explicitly pass `...` to `typer.Option(default=...)`:
{* docs_src/options/required/tutorial002_py310.py hl[7] *}
/// info
If you hadn't seen that `...` before: it is a special single value, it is part of Python and is called "Ellipsis".
///
That will tell **Typer** that it's still a *CLI option*, but it doesn't have a default value, and it's required.
/// tip
Again, prefer to use the `Annotated` version if possible. That way your code will mean the same in standard Python and in **Typer**.
///
And test it:
```console
// Pass the NAME CLI argument
$ python main.py Camila
// We didn't pass the now required --lastname CLI option
Usage: main.py [OPTIONS] NAME
Try "main.py --help" for help.
Error: Missing option '--lastname'.
// Now update it to pass the required --lastname CLI option
$ python main.py Camila --lastname Gutiérrez
Hello Camila Gutiérrez
// And if you check the help
$ python main.py --help
Usage: main.py [OPTIONS] NAME
Options:
--lastname TEXT [required]
--help Show this message and exit.
// It now tells you that --lastname is required 🎉
```
================================================
FILE: docs/tutorial/options/version.md
================================================
# Version CLI Option, `is_eager`
You could use a callback to implement a `--version` *CLI option*.
It would show the version of your CLI program and then it would terminate it. Even before any other *CLI parameter* is processed.
## First version of `--version`
Let's see a first version of how it could look like:
{* docs_src/options/version/tutorial001_an_py310.py hl[10:13,19:21] *}
/// tip
Notice that we don't have to get the `typer.Context` and check for `ctx.resilient_parsing` for completion to work, because we only print and modify the program when `--version` is passed, otherwise, nothing is printed or changed from the callback.
///
If the `--version` *CLI option* is passed, we get a value `True` in the callback.
Then we can print the version and raise `typer.Exit()` to make sure the program is terminated before anything else is executed.
We also declare the explicit *CLI option* name `--version`, because we don't want an automatic `--no-version`, it would look awkward.
Check it:
```console
$ python main.py --help
// We get a --version, and don't get an awkward --no-version 🎉
Usage: main.py [OPTIONS]
Options:
--version
--name TEXT
--help Show this message and exit.
// We can call it normally
$ python main.py --name Camila
Hello Camila
// And we can get the version
$ python main.py --version
Awesome CLI Version: 0.1.0
// Because we exit in the callback, we don't get a "Hello World" message after the version 🚀
```
## Previous parameters and `is_eager`
But now let's say that the `--name` *CLI option* that we declared before `--version` is required, and it has a callback that could exit the program:
{* docs_src/options/version/tutorial002_an_py310.py hl[16:19,25:27] *}
Then our CLI program could not work as expected in some cases as it is *right now*, because if we use `--version` after `--name` then the callback for `--name` will be processed before and we can get its error:
```console
$ python main.py --name Rick --version
Only Camila is allowed
Aborted!
```
/// tip
We don't have to check for `ctx.resilient_parsing` in the `name_callback()` for completion to work, because we are not using `typer.echo()`, instead we are raising a `typer.BadParameter`.
///
/// note | Technical Details
`typer.BadParameter` prints the error to "standard error", not to "standard output", and because the completion system only reads from "standard output", it won't break completion.
///
/// info
If you need a refresher about what is "standard output" and "standard error" check the section in [Printing and Colors: "Standard Output" and "Standard Error"](../printing.md#standard-output-and-standard-error){.internal-link target=_blank}.
///
### Fix with `is_eager`
For those cases, we can mark a *CLI parameter* (a *CLI option* or *CLI argument*) with `is_eager=True`.
That will tell **Typer** that it should process this *CLI parameter* before the others:
{* docs_src/options/version/tutorial003_an_py310.py hl[25:28] *}
Check it:
```console
$ python main.py --name Rick --version
// Now we only get the version, and the name is not used
Awesome CLI Version: 0.1.0
```
================================================
FILE: docs/tutorial/options-autocompletion.md
================================================
# CLI Option autocompletion
As you have seen, apps built with **Typer** have completion in your shell that works when you create a Python package or using the `typer` command.
It normally completes *CLI options*, *CLI arguments*, and subcommands (that you will learn about later).
But you can also provide auto completion for the **values** of *CLI options* and *CLI arguments*. We will learn about that here.
## Review completion
Before checking how to provide custom completions, let's check again how it works.
After installing completion for your own Python package (or using the `typer` command), when you use your CLI program and start adding a *CLI option* with `--` and then hit TAB, your shell will show you the available *CLI options* (the same for *CLI arguments*, etc).
To check it quickly without creating a new Python package, use the `typer` command.
Then let's create a small example program:
{* docs_src/options_autocompletion/tutorial001_an_py310.py *}
And let's try it with the `typer` command to get completion:
```console
// Hit the TAB key in your keyboard below where you see the: [TAB]
$ typer ./main.py [TAB][TAB]
// Depending on your terminal/shell you will get some completion like this ✨
run -- Run the provided Typer app.
utils -- Extra utility commands for Typer apps.
// Then try with "run" and --
$ typer ./main.py run --[TAB][TAB]
// You will get completion for --name, depending on your terminal it will look something like this
--name -- The name to say hi to.
// And you can run it as if it was with Python directly
$ typer ./main.py run --name Camila
Hello Camila
```
## Custom completion for values
Right now we get completion for the *CLI option* names, but not for the values.
We can provide completion for the values creating an `autocompletion` function, similar to the `callback` functions from [CLI Option Callback and Context](./options/callback-and-context.md){.internal-link target=_blank}:
{* docs_src/options_autocompletion/tutorial002_an_py310.py hl[6:7,16] *}
We return a `list` of strings from the `complete_name()` function.
And then we get those values when using completion:
```console
$ typer ./main.py run --name [TAB][TAB]
// We get the values returned from the function 🎉
Camila Carlos Sebastian
```
We got the basics working. Now let's improve it.
## Check the incomplete value
Right now, we always return those values, even if users start typing `Sebast` and then hit TAB, they will also get the completion for `Camila` and `Carlos` (depending on the shell), while we should only get completion for `Sebastian`.
But we can fix that so that it always works correctly.
Modify the `complete_name()` function to receive a parameter of type `str`, it will contain the incomplete value.
Then we can check and return only the values that start with the incomplete value from the command line:
{* docs_src/options_autocompletion/tutorial003_an_py310.py hl[8:13] *}
Now let's try it:
```console
$ typer ./main.py run --name Ca[TAB][TAB]
// We get the values returned from the function that start with Ca 🎉
Camila Carlos
```
Now we are only returning the valid values, that start with `Ca`, we are no longer returning `Sebastian` as a completion option.
/// tip
You have to declare the incomplete value of type `str` and that's what you will receive in the function.
No matter if the actual value will be an `int`, or something else, when doing completion, you will only get a `str` as the incomplete value.
And the same way, you can only return `str`, not `int`, etc.
///
## Add help to completions
Right now we are returning a `list` of `str`.
But some shells (Zsh, Fish, PowerShell) are capable of showing extra help text for completion.
We can provide that extra help text so that those shells can show it.
In the `complete_name()` function, instead of providing one `str` per completion element, we provide a `tuple` with 2 items. The first item is the actual completion string, and the second item is the help text.
So, in the end, we return a `list` of `tuples` of `str`:
{* docs_src/options_autocompletion/tutorial004_an_py310.py hl[5:9,12:18] *}
/// tip
If you want to have help text for each item, make sure each item in the list is a `tuple`. Not a `list`.
In the end, the return will be a `list` (or other iterable) of `tuples` of 2 `str`.
///
/// info
The help text will be visible in Zsh, Fish, and PowerShell.
Bash doesn't support showing the help text, but completion will still work the same.
///
If you have a shell like Zsh, it would look like:
```console
$ typer ./main.py run --name [TAB][TAB]
// We get the completion items with their help text 🎉
Camila -- The reader of books.
Carlos -- The writer of scripts.
Sebastian -- The type hints guy.
```
## Simplify with `yield`
Instead of creating and returning a list with values (`str` or `tuple`), we can use `yield` with each value that we want in the completion.
That way our function will be a generator that **Typer** can iterate:
{* docs_src/options_autocompletion/tutorial005_an_py310.py hl[12:15] *}
That simplifies our code a bit and works the same.
/// tip
If the `yield` part seems complex for you, don't worry, you can just use the version with the `list` above.
In the end, that's just to save us a couple of lines of code.
///
/// info
The function can use `yield`, so it doesn't have to return strictly a `list`, it just has to be iterable.
But each of the elements for completion has to be a `str` or a `tuple` (when containing a help text).
///
## Access other *CLI parameters* with the Context
Let's say that now we want to modify the program to be able to "say hi" to multiple people at the same time.
So, we will allow multiple `--name` *CLI options*.
/// tip
You will learn more about *CLI parameters* with multiple values later in the tutorial.
So, for now, take this as a sneak peek 😉.
///
For this we use a `list` of `str`:
{* docs_src/options_autocompletion/tutorial006_an_py310.py hl[8:13] *}
And then we can use it like:
```console
$ typer ./main.py run --name Camila --name Sebastian
Hello Camila
Hello Sebastian
```
### Getting completion for multiple values
And the same way as before, we want to provide **completion** for those names. But we don't want to provide the **same names** for completion if they were already given in previous parameters.
For that, we will access and use the "Context". Every Typer application has a special object called a "Context" that is normally hidden.
But you can access the context by declaring a function parameter of type `typer.Context`.
And from that context you can get the current values for each parameter.
{* docs_src/options_autocompletion/tutorial007_an_py310.py hl[12:13,15] *}
We are getting the `names` already provided with `--name` in the command line before this completion was triggered.
If there's no `--name` in the command line, it will be `None`, so we use `or []` to make sure we have a `list` (even if empty) to check its contents later.
Then, when we have a completion candidate, we check if each `name` was already provided with `--name` by checking if it's in that list of `names` with `name not in names`.
And then we `yield` each item that has not been used yet.
Check it:
```console
$ typer ./main.py run --name [TAB][TAB]
// The first time we trigger completion, we get all the names
Camila -- The reader of books.
Carlos -- The writer of scripts.
Sebastian -- The type hints guy.
// Add a name and trigger completion again
$ typer ./main.py run --name Sebastian --name Ca[TAB][TAB]
// Now we get completion only for the names we haven't used 🎉
Camila -- The reader of books.
Carlos -- The writer of scripts.
// And if we add another of the available names:
$ typer ./main.py run --name Sebastian --name Camila --name [TAB][TAB]
// We get completion for the only available one
Carlos -- The writer of scripts.
```
/// tip
It's quite possible that if there's only one option left, your shell will complete it right away instead of showing the option with the help text, to save you more typing.
///
## Getting the raw *CLI parameters*
You can also get the raw *CLI parameters*, just a `list` of `str` with everything passed in the command line before the incomplete value.
For example, something like `["typer", "main.py", "run", "--name"]`.
/// tip
This would be for advanced scenarios, in most use cases you would be better off using the context.
But it's still possible if you need it.
///
As a simple example, let's show it on the screen before completion.
Because completion is based on the output printed by your program (handled internally by **Typer**), during completion we can't just print something else as we normally do.
### Printing to "standard error"
/// tip
If you need a refresher about what is "standard output" and "standard error" check the section in [Printing and Colors: "Standard Output" and "Standard Error"](./printing.md#standard-output-and-standard-error){.internal-link target=_blank}.
///
The completion system only reads from "standard output", so, printing to "standard error" won't break completion. 🚀
You can print to "standard error" with a **Rich** `Console(stderr=True)`.
Using `stderr=True` tells **Rich** that the output should be shown in "standard error".
{* docs_src/options_autocompletion/tutorial008_an_py310.py hl[12,15:16] *}
/// info
If you have disabled Rich, you can also use `print(lastname, file=sys.stderr)` or `typer.echo("some text", err=True)` instead.
///
We get all the *CLI parameters* as a raw `list` of `str` by declaring a parameter with type `list[str]`, here it's named `args`.
/// tip
Here we name the list of all the raw *CLI parameters* `args` because that's the usual convention.
But it doesn't contain only *CLI arguments*, it has everything, including *CLI options* and values, as a raw `list` of `str`.
///
And then we just print it to "standard error".
```console
$ typer ./main.py run --name [TAB][TAB]
// First we see the raw CLI parameters
['./main.py', 'run', '--name']
// And then we see the actual completion
Camila -- The reader of books.
Carlos -- The writer of scripts.
Sebastian -- The type hints guy.
```
/// tip
This is a very simple (and quite useless) example, just so you know how it works and that you can use it.
But it's probably useful only in very advanced use cases.
///
## Getting the Context and the raw *CLI parameters*
Of course, you can declare everything if you need it, the context, the raw *CLI parameters*, and the incomplete `str`:
{* docs_src/options_autocompletion/tutorial009_an_py310.py hl[15] *}
Check it:
```console
$ typer ./main.py run --name [TAB][TAB]
// First we see the raw CLI parameters
['./main.py', 'run', '--name']
// And then we see the actual completion
Camila -- The reader of books.
Carlos -- The writer of scripts.
Sebastian -- The type hints guy.
$ typer ./main.py run --name Sebastian --name Ca[TAB][TAB]
// Again, we see the raw CLI parameters
['./main.py', 'run', '--name', 'Sebastian', '--name']
// And then we see the rest of the valid completion items
Camila -- The reader of books.
Carlos -- The writer of scripts.
```
## Types, types everywhere
**Typer** uses the type declarations to detect what it has to provide to your `autocompletion` function.
You can declare function parameters of these types:
* `str`: for the incomplete value.
* `typer.Context`: for the current context.
* `list[str]`: for the raw *CLI parameters*.
It doesn't matter how you name them, in which order, or which ones of the 3 options you declare. It will all "**just work**" ✨
================================================
FILE: docs/tutorial/package.md
================================================
# Building a Package
When you create a CLI program with **Typer** you probably want to create your own Python package.
That's what allows your users to install it and have it as an independent program that they can use in their terminal.
And that's also required for shell auto completion to work (unless you use your program through the `typer` command).
Nowadays, there are several ways and tools to create Python packages (what you install with `pip install something` or `uv add something`).
You might even have your favorite already.
Here's a very opinionated, short guide, showing one of the alternative ways of creating a Python package with a **Typer** app, from scratch.
/// tip
If you already have a favorite way of creating Python packages, feel free to skip this.
///
## Prerequisites
For this guide we'll use uv.
uv's docs are great, so go ahead, check them and install it.
## Create a project
Let's say we want to create a CLI application called `portal-gun`.
To make sure your package doesn't collide with the package created by someone else, we'll name it with a prefix of your name.
So, if your name is Rick, we'll call it `rick-portal-gun`.
Create a project with uv:
```console
$ uv init --package rick-portal-gun
Initialized project `rick-portal-gun` at `/home/rick-portal-gun`
// Enter the new project directory
cd ./rick-portal-gun
```
## Dependencies and environment
Add `typer` to your dependencies:
```console
$ uv add typer
// It creates a virtual environment for your project
Using CPython 3.14.0 interpreter at: /location/of/python/
Creating virtual environment at: .venv
Resolved 10 packages in 21ms
Built rick-portal-gun @ file:/home/rick-portal-gun
Prepared 1 package in 19ms
Installed 10 packages in 34ms
+ click==8.3.1
+ colorama==0.4.6
+ markdown-it-py==4.0.0
+ mdurl==0.1.2
+ pygments==2.19.2
+ rich==14.2.0
+ rick-portal-gun==0.1.0 (from file:/home/rick-portal-gun)
+ shellingham==1.5.4
+ typer==0.21.0
+ typing-extensions==4.15.0
// Activate that new virtual environment
$ source .venv/bin/activate
// Open an editor using this new environment, for example VS Code
$ code ./
```
You can see that you have a generated project structure that looks like:
```
.
├── pyproject.toml
├── README.md
├── src
│ └── rick_portal_gun
│ └── __init__.py
└── uv.lock
```
## Create your app
Now let's create an extremely simple **Typer** app.
Create a file `src/rick_portal_gun/main.py` with:
```Python
import typer
app = typer.Typer()
@app.callback()
def callback():
"""
Awesome Portal Gun
"""
@app.command()
def shoot():
"""
Shoot the portal gun
"""
typer.echo("Shooting portal gun")
@app.command()
def load():
"""
Load the portal gun
"""
typer.echo("Loading portal gun")
```
/// tip
As we are creating an installable Python package, there's no need to add a section with `if __name__ == "__main__":`.
///
## Modify the README
Let's change the README to have something like:
```Markdown
# Portal Gun
The awesome Portal Gun
```
## Add a "script"
We are creating a Python package that can be installed with `uv add` or `pip install`.
But we want it to provide a CLI program that can be executed in the shell.
To do that, we add a configuration to the `pyproject.toml` in the section `[project.scripts]`:
```TOML hl_lines="12 13"
[project]
name = "rick-portal-gun"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
authors = ["Rick Sanchez "]
requires-python = ">=3.14"
dependencies = [
"typer>=0.21.0",
]
[project.scripts]
rick-portal-gun = "rick_portal_gun.main:app"
[build-system]
requires = ["uv_build>=0.8.14,<0.9.0"]
build-backend = "uv_build"
```
Here's what that line means:
`rick-portal-gun`: will be the name of the CLI program. That's how we will call it in the terminal once it is installed. Like:
```console
$ rick-portal-gun
// Something happens here ✨
```
`rick_portal_gun.main`, in the part `"rick_portal_gun.main:app"`, with underscores, refers to the Python module to import. That's what someone would use in a section like:
```Python
from rick_portal_gun.main import # something goes here
```
The `app` in `"rick_portal_gun.main:app"` is the thing to import from the module, and to call as a function, like:
```Python
from rick_portal_gun.main import app
app()
```
That config section tells uv that when this package is installed, we want it to create a command line program called `rick-portal-gun`.
And that the object to call (like a function) is the one in the variable `app` inside of the module `rick_portal_gun.main`.
## Install your package
That's what we need to create a package.
You can now install it:
```console
$ uv sync
Resolved 10 packages in 1ms
Built rick-portal-gun @ file:/home/rick-portal-gun
Prepared 1 package in 18ms
Uninstalled 1 package in 1ms
Installed 1 package in 13ms
~ rick-portal-gun==0.1.0 (from file:/home/rick-portal-gun)
```
## Try your CLI program
Your package is installed in the environment created by uv, but you can already use it.
```console
// You can use the which program to check which rick-portal-gun program is available (if any)
$ which rick-portal-gun
// You get the one from your environment
/home/rick-portal-gun/.venv/bin/rick-portal-gun
// Try it
$ rick-portal-gun --help
// You get all the standard help
Usage: rick-portal-gun [OPTIONS] COMMAND [ARGS]...
Awesome Portal Gun
Options:
--install-completion Install completion for the current shell.
--show-completion Show completion for the current shell, to copy it or customize the installation.
--help Show this message and exit.
Commands:
shoot Shoot the portal gun
load Load the portal gun
```
## Create a wheel package
Python packages have a standard format called a "wheel". It's a file that ends in `.whl`.
You can create a wheel with uv:
```console
$ uv build
Building source distribution (uv build backend)...
Building wheel from source distribution (uv build backend)...
Successfully built dist/rick_portal_gun-0.1.0.tar.gz
Successfully built dist/rick_portal_gun-0.1.0-py3-none-any.whl
```
After that, if you check in your project directory, you should now have a couple of extra files at `./dist/`:
``` hl_lines="3 4"
.
├── dist
│ ├── rick_portal_gun-0.1.0-py3-none-any.whl
│ └── rick-portal-gun-0.1.0.tar.gz
├── pyproject.toml
├── README.md
├── ...
```
The `.whl` is the wheel file. You can send that wheel file to anyone and they can use it to install your program (we'll see how to upload it to PyPI in a bit).
## Test your wheel package
Now you can open another terminal and install that package from the file for your own user with:
```console
$ pip install --user /home/rick/rick-portal-gun/dist/rick_portal_gun-0.1.0-py3-none-any.whl
---> 100%
```
/// warning
The `--user` is important, that ensures you install it in your user's directory and not in the global system.
If you installed it in the global system (e.g. with `sudo`) you could install a version of a library (e.g. a sub-dependency) that is incompatible with your system.
///
/// tip
Bonus points if you use uvx to install it while keeping an isolated environment for your Python CLI programs 🚀
///
Now you have your CLI program installed. And you can use it freely:
```console
$ rick-portal-gun shoot
// It works 🎉
Shooting portal gun
```
Having it installed globally (and not in a single environment), you can now install completion globally for it:
```console
$ rick-portal-gun --install-completion
zsh completion installed in /home/rick/.zshrc.
Completion will take effect once you restart the terminal.
```
/// tip
If you want to remove completion you can just delete the added line in that file.
///
And after you restart the terminal you will get completion for your new CLI program:
```console
$ rick-portal-gun [TAB][TAB]
// You get completion for your CLI program ✨
load -- Load the portal gun
shoot -- Shoot the portal gun
```
## Support `python -m` (optional)
You may have seen that you can call many Python modules as scripts with `python -m some-module`.
For example, one way to call `pip` is:
```console
$ pip install fastapi
```
But you can also call Python with the `-m` *CLI Option* and pass a module for it to execute as if it was a script, like:
```console
$ python -m pip install fastapi
```
Here we pass `pip` as the value for `-m`, so, Python will execute the module `pip` as if it was a script. And then it will pass the rest of the *CLI Parameters* (`install fastapi`) to it.
These two are more or less equivalent, the `install fastapi` will be passed to `pip`.
/// tip
In the case of `pip`, in many occasions it's actually recommended that you run it with `python -m`, because if you create a virtual environment with its own `python`, that will ensure that you use the `pip` from *that* environment.
///
### Add a `__main__.py`
You can support that same style of calling the package/module for your own package, simply by adding a file `__main__.py`.
Python will look for that file and execute it.
The file would live right beside `__init__.py` and `main.py`:
``` hl_lines="7"
.
├── pyproject.toml
├── README.md
├── src
│ └── rick_portal_gun
│ ├── __init__.py
│ ├── __main__.py
│ └── main.py
└── uv.lock
```
No other file has to import it, you don't have to reference it in your `pyproject.toml` or anything else, it just works by default, as it is standard Python behavior.
Then in that file you can execute your **Typer** program:
```Python
from .main import app
app()
```
Now, after installing your package, if you call it with `python -m` it will work:
```console
$ python -m rick_portal_gun --help
Usage: python -m rick_portal_gun [OPTIONS] COMMAND [ARGS]...
Awesome Portal Gun
Options:
--install-completion Install completion for the current shell.
--show-completion Show completion for the current shell, to copy it or customize the installation.
--help Show this message and exit.
Commands:
shoot Shoot the portal gun
load Load the portal gun
```
/// tip
Notice that you have to pass the importable version of the package name, so `rick_portal_gun` instead of `rick-portal-gun`.
///
That works! 🚀
### Autocompletion and `python -m`
Have in mind that TAB completion (shell auto-completion) won't work when using `python -m`.
Auto-completion depends on the name of the program called, it's tied to each specific program name.
So, to have shell completion for `rick-portal-gun` you would have to call it directly:
```console
$ rick-portal-gun [TAB][TAB]
```
But you can still support `python -m` for the cases where it's useful.
## Publish to PyPI (optional)
You can publish that new package to PyPI to make it public, so others can install it easily.
So, go ahead and create an account there (it's free).
### PyPI API token
To do it, you first need to configure a PyPI auth token.
Login to PyPI.
And then go to https://pypi.org/manage/account/token/ to create a new token.
Let's say your new API token is:
```
pypi-wubalubadubdub-deadbeef1234
```
Now configure uv to use this token by setting an environment variable:
```console
$ export UV_PUBLISH_TOKEN=pypi-wubalubadubdub-deadbeef1234
// It won't show any output, but it's already configured
```
### Publish to PyPI
Now you can publish your package.
```console
$ uv publish
Publishing 2 files https://upload.pypi.org/legacy/
Uploading rick_portal_gun-0.1.0-py3-none-any.whl (2.3KiB)
Uploading rick_portal_gun-0.1.0.tar.gz (841.0B)
```
Now you can go to PyPI and check your projects at https://pypi.org/manage/projects/.
You should now see your new "rick-portal-gun" package.
### Install from PyPI
Now to see that we can install it from PyPI, open another terminal, and uninstall the currently installed package.
```console
$ pip uninstall rick-portal-gun
Found existing installation: rick-portal-gun 0.1.0
Uninstalling rick-portal-gun-0.1.0:
Would remove:
/home/rick/.local/bin/rick-portal-gun
/home/rick/.local/lib/python3.10/site-packages/rick_portal_gun-0.1.0.dist-info/*
/home/rick/.local/lib/python3.10/site-packages/rick_portal_gun/*
# Proceed (Y/n)? $ Y
Successfully uninstalled rick-portal-gun-0.1.0
```
And now install it again, but this time using just the name, so that `pip` pulls it from PyPI:
```console
$ pip install --user rick-portal-gun
// Notice that it says "Downloading" 🚀
Collecting rick-portal-gun
Downloading rick_portal_gun-0.1.0-py3-none-any.whl.metadata (435 bytes)
Requirement already satisfied: typer<0.13.0,>=0.12.3 in ./.local/lib/python3.10/site-packages (from rick-portal-gun==0.1.0) (0.12.3)
Requirement already satisfied: typing-extensions>=3.7.4.3 in ./.local/lib/python3.10/site-packages (from typer<0.13.0,>=0.12.3->rick-portal-gun==0.1.0) (4.11.0)
Requirement already satisfied: click>=8.0.0 in ./.local/lib/python3.10/site-packages (from typer<0.13.0,>=0.12.3->rick-portal-gun==0.1.0) (8.1.7)
Requirement already satisfied: shellingham>=1.3.0 in ./.local/lib/python3.10/site-packages (from typer<0.13.0,>=0.12.3->rick-portal-gun==0.1.0) (1.5.4)
Requirement already satisfied: rich>=10.11.0 in ./.local/lib/python3.10/site-packages (from typer<0.13.0,>=0.12.3->rick-portal-gun==0.1.0) (13.7.1)
Requirement already satisfied: pygments<3.0.0,>=2.13.0 in ./.local/lib/python3.10/site-packages (from rich>=10.11.0->typer<0.13.0,>=0.12.3->rick-portal-gun==0.1.0) (2.17.2)
Requirement already satisfied: markdown-it-py>=2.2.0 in ./.local/lib/python3.10/site-packages (from rich>=10.11.0->typer<0.13.0,>=0.12.3->rick-portal-gun==0.1.0) (3.0.0)
Requirement already satisfied: mdurl~=0.1 in ./.local/lib/python3.10/site-packages (from markdown-it-py>=2.2.0->rich>=10.11.0->typer<0.13.0,>=0.12.3->rick-portal-gun==0.1.0) (0.1.2)
Downloading rick_portal_gun-0.1.0-py3-none-any.whl (1.8 kB)
Installing collected packages: rick-portal-gun
Successfully installed rick-portal-gun-0.1.0
```
And now test the newly installed package from PyPI:
```console
$ rick-portal-gun load
// It works! 🎉
Loading portal gun
```
## Generate docs
You can use the `typer` command to generate docs for your package that you can put in your `README.md`:
```console
$ typer rick_portal_gun.main utils docs --output README.md --name rick-portal-gun
Docs saved to: README.md
```
You just have to pass it the module to import (`rick_portal_gun.main`) and it will detect the `typer.Typer` app automatically.
By specifying the `--name` of the program it will be able to use it while generating the docs.
### Publish a new version with the docs
Now you can publish a new version with the updated docs.
For that you need to first increase the version in `pyproject.toml`:
```TOML hl_lines="3"
[project]
name = "rick-portal-gun"
version = "0.2.0"
description = "Add your description here"
readme = "README.md"
authors = ["Rick Sanchez "]
requires-python = ">=3.14"
dependencies = [
"typer>=0.21.0",
]
[project.scripts]
rick-portal-gun = "rick_portal_gun.main:app"
[build-system]
requires = ["uv_build>=0.8.14,<0.9.0"]
build-backend = "uv_build"
```
And then build and publish again:
```console
$ uv build
$ uv publish
Publishing 2 files https://upload.pypi.org/legacy/
Uploading rick_portal_gun-0.2.0-py3-none-any.whl (2.3KiB)
Uploading rick_portal_gun-0.2.0.tar.gz (840.0B)
```
And now you can go to PyPI, to the project page, and reload it, and it will now have your new generated docs.
## What's next
This is a very simple guide. You could add many more steps.
For example, you should use Git, the version control system, to save your code.
You could use uv to manage your installed CLI Python programs in isolated environments.
Maybe use automatic formatting with Ruff.
You'll probably want to publish your code as open source to GitHub.
And then you could integrate a CI tool to run your tests and deploy your package automatically.
And there's a long etc. But now you have the basics and you can continue on your own. 🚀
================================================
FILE: docs/tutorial/parameter-types/bool.md
================================================
# Boolean CLI Options
We have seen some examples of *CLI options* with `bool`, and how **Typer** creates `--something` and `--no-something` automatically.
But we can customize those names.
## Only `--force`
Let's say that we want a `--force` *CLI option* only, we want to discard `--no-force`.
We can do that by specifying the exact name we want:
{* docs_src/parameter_types/bool/tutorial001_an_py310.py hl[9] *}
Now there's only a `--force` *CLI option*:
```console
// Check the help
$ python main.py --help
// Notice there's only --force, we no longer have --no-force
Usage: main.py [OPTIONS]
Options:
--force [default: False]
--help Show this message and exit.
// Try it:
$ python main.py
Not forcing
// Now add --force
$ python main.py --force
Forcing operation
// And --no-force no longer exists ⛔️
$ python main.py --no-force
Usage: main.py [OPTIONS]
Try "main.py --help" for help.
Error: No such option: --no-force
```
## Alternative names
Now let's imagine we have a *CLI option* `--accept`.
And we want to allow setting `--accept` or the contrary, but `--no-accept` looks ugly.
We might want to instead have `--accept` and `--reject`.
We can do that by passing a single `str` with the 2 names for the `bool` *CLI option* separated by `/`:
{* docs_src/parameter_types/bool/tutorial002_an_py310.py hl[9] *}
Check it:
```console
// Check the help
$ python main.py --help
// Notice the --accept / --reject
Usage: main.py [OPTIONS]
Options:
--accept / --reject
--help Show this message and exit.
// Try it
$ python main.py
I don't know what you want yet
// Now pass --accept
$ python main.py --accept
Accepting!
// And --reject
$ python main.py --reject
Rejecting!
```
## Short names
The same way, you can declare short versions of the names for these *CLI options*.
For example, let's say we want `-f` for `--force` and `-F` for `--no-force`:
{* docs_src/parameter_types/bool/tutorial003_an_py310.py hl[9] *}
Check it:
```console
// Check the help
$ python main.py --help
// Notice the -f, --force / -F, --no-force
Usage: main.py [OPTIONS]
Options:
-f, --force / -F, --no-force [default: False]
--help Show this message and exit.
// Try with the short name -f
$ python main.py -f
Forcing operation
// Try with the short name -F
$ python main.py -F
Not forcing
```
## Only names for `False`
If you want to (although it might not be a good idea), you can declare only *CLI option* names to set the `False` value.
To do that, use a space and a single `/` and pass the negative name after:
{* docs_src/parameter_types/bool/tutorial004_an_py310.py hl[9] *}
/// tip
Have in mind that it's a string with a preceding space and then a `/`.
So, it's `" /-S"` not `"/-S"`.
///
Check it:
```console
// Check the help
$ python main.py --help
// Notice the / -d, --demo
Usage: main.py [OPTIONS]
Options:
/ -d, --demo [default: True]
--help Show this message and exit.
// Try it
$ python main.py
Running in production
// Now pass --demo
$ python main.py --demo
Running demo
// And the short version
$ python main.py -d
Running demo
```
================================================
FILE: docs/tutorial/parameter-types/custom-types.md
================================================
# Custom Types
You can easily use your own custom types in your **Typer** applications.
The way to do it is by providing a way to parse input into your own types.
## Type Parser
`typer.Argument` and `typer.Option` can create custom parameter types with a `parser` callable.
{* docs_src/parameter_types/custom_types/tutorial001_an_py310.py hl[14:15,23:24] *}
The function (or callable) that you pass to the parameter `parser` will receive the input value as a string and should return the parsed value with your own custom type.
================================================
FILE: docs/tutorial/parameter-types/datetime.md
================================================
# DateTime
You can specify a *CLI parameter* as a Python `datetime`.
Your function will receive a standard Python `datetime` object, and again, your editor will give you completion, etc.
{* docs_src/parameter_types/datetime/tutorial001_py310.py hl[1,9,10,11] *}
Typer will accept any string from the following formats:
* `%Y-%m-%d`
* `%Y-%m-%dT%H:%M:%S`
* `%Y-%m-%d %H:%M:%S`
Check it:
```console
$ python main.py --help
Usage: main.py [OPTIONS] BIRTH:[%Y-%m-%d|%Y-%m-%dT%H:%M:%S|%Y-%m-%d %H:%M:%S]
Arguments:
BIRTH:[%Y-%m-%d|%Y-%m-%dT%H:%M:%S|%Y-%m-%d %H:%M:%S][required]
Options:
--help Show this message and exit.
// Pass a datetime
$ python main.py 1956-01-31T10:00:00
Interesting day to be born: 1956-01-31 10:00:00
Birth hour: 10
// An invalid date
$ python main.py july-19-1989
Usage: main.py [OPTIONS] [%Y-%m-%d|%Y-%m-%dT%H:%M:%S|%Y-%m-%d%H:%M:%S]
Error: Invalid value for 'BIRTH:[%Y-%m-%d|%Y-%m-%dT%H:%M:%S|%Y-%m-%d %H:%M:%S]': 'july-19-1989' does not match the formats '%Y-%m-%d', '%Y-%m-%dT%H:%M:%S', '%Y-%m-%d %H:%M:%S'.
```
## Custom date format
You can also customize the formats received for the `datetime` with the `formats` parameter.
`formats` receives a list of strings with the date formats that would be passed to datetime.strptime().
For example, let's imagine that you want to accept an ISO formatted datetime, but for some strange reason, you also want to accept a format with:
* first the month
* then the day
* then the year
* separated with "`/`"
...It's a crazy example, but let's say you also needed that strange format:
{* docs_src/parameter_types/datetime/tutorial002_an_py310.py hl[14] *}
/// tip
Notice the last string in `formats`: `"%m/%d/%Y"`.
///
Check it:
```console
// ISO dates work
$ python main.py 1969-10-29
Launch will be at: 1969-10-29 00:00:00
// But the strange custom format also works
$ python main.py 10/29/1969
Launch will be at: 1969-10-29 00:00:00
```
================================================
FILE: docs/tutorial/parameter-types/enum.md
================================================
# Enum - Choices
To define a *CLI parameter* that can take a value from a predefined set of values you can use a standard Python `enum.Enum`:
{* docs_src/parameter_types/enum/tutorial001_py310.py hl[1,6:9,16:17] *}
/// tip
Notice that the function parameter `network` will be an `Enum`, not a `str`.
To get the `str` value in your function's code use `network.value`.
///
Check it:
```console
$ python main.py --help
// Notice the predefined values [simple|conv|lstm]
Usage: main.py [OPTIONS]
Options:
--network [simple|conv|lstm] [default: simple]
--help Show this message and exit.
// Try it
$ python main.py --network conv
Training neural network of type: conv
// Invalid value
$ python main.py --network capsule
Usage: main.py [OPTIONS]
Try "main.py --help" for help.
Error: Invalid value for '--network': 'capsule' is not one of 'simple', 'conv', 'lstm'.
// Note that enums are case sensitive by default
$ python main.py --network CONV
Usage: main.py [OPTIONS]
Try "main.py --help" for help.
Error: Invalid value for '--network': 'CONV' is not one of 'simple', 'conv', 'lstm'.
```
### Case insensitive Enum choices
You can make an `Enum` (choice) *CLI parameter* be case-insensitive with the `case_sensitive` parameter:
{* docs_src/parameter_types/enum/tutorial002_an_py310.py hl[19] *}
And then the values of the `Enum` will be checked no matter if lower case, upper case, or a mix:
```console
// Notice the upper case CONV
$ python main.py --network CONV
Training neural network of type: conv
// A mix also works
$ python main.py --network LsTm
Training neural network of type: lstm
```
### List of Enum values
A *CLI parameter* can also take a list of `Enum` values:
{* docs_src/parameter_types/enum/tutorial003_an_py310.py hl[17] *}
This works just like any other parameter value taking a list of things:
```console
$ python main.py --help
// Notice the default values being shown
Usage: main.py [OPTIONS]
Options:
--groceries [Eggs|Bacon|Cheese] [default: Eggs, Cheese]
--help Show this message and exit.
// Try it with the default values
$ python main.py
Buying groceries: Eggs, Cheese
// Try it with a single value
$ python main.py --groceries "Eggs"
Buying groceries: Eggs
// Try it with multiple values
$ python main.py --groceries "Eggs" --groceries "Bacon"
Buying groceries: Eggs, Bacon
```
### Literal choices
You can also use `Literal` to represent a set of possible predefined choices, without having to use an `Enum`:
{* docs_src/parameter_types/enum/tutorial004_an_py310.py hl[10] *}
```console
$ python main.py --help
// Notice the predefined values [simple|conv|lstm]
Usage: main.py [OPTIONS]
Options:
--network [simple|conv|lstm] [default: simple]
--help Show this message and exit.
// Try it
$ python main.py --network conv
Training neural network of type: conv
// Invalid value
$ python main.py --network capsule
Usage: main.py [OPTIONS]
Try "main.py --help" for help.
Error: Invalid value for '--network': 'capsule' is not one of 'simple', 'conv', 'lstm'.
```
================================================
FILE: docs/tutorial/parameter-types/file.md
================================================
# File
Apart from `Path` *CLI parameters* you can also declare some types of "files".
/// tip
In most of the cases you are probably fine just using `Path`.
You can read and write data with `Path` the same way.
///
The difference is that these types will give you a Python file-like object instead of a Python Path.
A "file-like object" is the same type of object returned by `open()` as in:
```Python
with open('file.txt') as f:
# Here f is the file-like object
read_data = f.read()
print(read_data)
```
But in some special use cases you might want to use these special types. For example if you are migrating an existing application.
## `FileText` reading
`typer.FileText` gives you a file-like object for reading text, you will get `str` data from it.
This means that even if your file has text written in a non-english language, e.g. a `text.txt` file with:
```
la cigüeña trae al niño
```
You will have a `str` with the text inside, e.g.:
```Python
content = "la cigüeña trae al niño"
```
instead of having `bytes`, e.g.:
```Python
content = b"la cig\xc3\xbce\xc3\xb1a trae al ni\xc3\xb1o"
```
You will get all the correct editor support, attributes, methods, etc for the file-like object:`
{* docs_src/parameter_types/file/tutorial001_an_py310.py hl[9] *}
Check it:
```console
// Create a quick text config
$ echo "some settings" > config.txt
// Add another line to the config to test it
$ echo "some more settings" >> config.txt
// Now run your program
$ python main.py --config config.txt
Config line: some settings
Config line: some more settings
```
## `FileTextWrite`
For writing text, you can use `typer.FileTextWrite`:
{* docs_src/parameter_types/file/tutorial002_an_py310.py hl[9:10] *}
This would be for writing human text, like:
```
some settings
la cigüeña trae al niño
```
...not to write binary `bytes`.
Check it:
```console
$ python main.py --config text.txt
Config written
// Check the contents of the file
$ cat text.txt
Some config written by the app
```
/// info | Technical Details
`typer.FileTextWrite` is a just a convenience class.
It's the same as using `typer.FileText` and setting `mode="w"`. You will learn about `mode` later below.
///
## `FileBinaryRead`
To read binary data you can use `typer.FileBinaryRead`.
You will receive `bytes` from it.
It's useful for reading binary files like images:
{* docs_src/parameter_types/file/tutorial003_an_py310.py hl[9] *}
Check it:
```console
$ python main.py --file lena.jpg
Processed bytes total: 512
Processed bytes total: 1024
Processed bytes total: 1536
Processed bytes total: 2048
```
## `FileBinaryWrite`
To write binary data you can use `typer.FileBinaryWrite`.
You would write `bytes` to it.
It's useful for writing binary files like images.
Have in mind that you have to pass `bytes` to its `.write()` method, not `str`.
If you have a `str`, you have to encode it first to get `bytes`.
{* docs_src/parameter_types/file/tutorial004_an_py310.py hl[9] *}
```console
$ python main.py --file binary.dat
Binary file written
// Check the binary file was created
$ ls ./binary.dat
./binary.dat
```
## File *CLI parameter* configurations
You can use several configuration parameters for these types (classes) in `typer.Option()` and `typer.Argument()`:
* `mode`: controls the "mode" to open the file with.
* It's automatically set for you by using the classes above.
* Read more about it below.
* `encoding`: to force a specific encoding, e.g. `"utf-8"`.
* `lazy`: delay I/O operations. Automatic by default.
* By default, when writing files, Typer will generate a file-like object that is not yet the actual file. Once you start writing, it will go, open the file and start writing to it, but not before. This is mainly useful to avoid creating the file until you start writing to it. It's normally safe to leave this automatic. But you can overwrite it setting `lazy=False`. By default, it's `lazy=True` for writing and `lazy=False` for reading.
* `atomic`: if true, all writes will actually go to a temporal file and then moved to the final destination after completing. This is useful with files modified frequently by several users/programs.
## Advanced `mode`
By default, **Typer** will configure the `mode` for you:
* `typer.FileText`: `mode="r"`, to read text.
* `typer.FileTextWrite`: `mode="w"`, to write text.
* `typer.FileBinaryRead`: `mode="rb"`, to read binary data.
* `typer.FileBinaryWrite`: `mode="wb"`, to write binary data.
### Note about `FileTextWrite`
`typer.FileTextWrite` is actually just a convenience class. It's the same as using `typer.FileText` with `mode="w"`.
But it's probably shorter and more intuitive as you can get it with autocompletion in your editor by just starting to type `typer.File`... just like the other classes.
### Customize `mode`
You can override the `mode` from the defaults above.
For example, you could use `mode="a"` to write "appending" to the same file:
{* docs_src/parameter_types/file/tutorial005_an_py310.py hl[9] *}
/// tip
As you are manually setting `mode="a"`, you can use `typer.FileText` or `typer.FileTextWrite`, both will work.
///
Check it:
```console
$ python main.py --config config.txt
Config line written
// Run your program a couple more times to see how it appends instead of overwriting
$ python main.py --config config.txt
Config line written
$ python main.py --config config.txt
Config line written
// Check the contents of the file, it should have each of the 3 lines appended
$ cat config.txt
This is a single line
This is a single line
This is a single line
```
================================================
FILE: docs/tutorial/parameter-types/index.md
================================================
# CLI Parameter Types
You can use several data types for the *CLI options* and *CLI arguments*, and you can add data validation requirements too.
## Data conversion
When you declare a *CLI parameter* with some type **Typer** will convert the data received in the command line to that data type.
For example:
{* docs_src/parameter_types/index/tutorial001_py310.py hl[7] *}
In this example, the value received for the *CLI argument* `NAME` will be treated as `str`.
The value for the *CLI option* `--age` will be converted to an `int` and `--height-meters` will be converted to a `float`.
And as `female` is a `bool` *CLI option*, **Typer** will convert it to a "flag" `--female` and the counterpart `--no-female`.
And here's how it looks like:
```console
$ python main.py --help
// Notice how --age is an INTEGER and --height-meters is a FLOAT
Usage: main.py [OPTIONS] NAME
Arguments:
NAME [required]
Options:
--age INTEGER [default: 20]
--height-meters FLOAT [default: 1.89]
--female / --no-female [default: True]
--help Show this message and exit.
// Call it with CLI parameters
$ python main.py Camila --age 15 --height-meters 1.70 --female
// All the data has the correct Python type
NAME is Camila, of type: class 'str'
--age is 15, of type: class 'int'
--height-meters is 1.7, of type: class 'float'
--female is True, of type: class 'bool'
// And if you pass an incorrect type
$ python main.py Camila --age 15.3
Usage: main.py [OPTIONS] NAME
Try "main.py --help" for help.
Error: Invalid value for '--age': '15.3' is not a valid integer
// Because 15.3 is not an INTEGER (it's a float)
```
## Watch next
See more about specific types and validations in the next sections...
================================================
FILE: docs/tutorial/parameter-types/number.md
================================================
# Number
You can define numeric validations with `max` and `min` values for `int` and `float` *CLI parameters*:
{* docs_src/parameter_types/number/tutorial001_an_py310.py hl[10:12] *}
*CLI arguments* and *CLI options* can both use these validations.
You can specify `min`, `max` or both.
Check it:
```console
$ python main.py --help
// Notice the extra RANGE in the help text for --age and --score
Usage: main.py [OPTIONS] ID
Arguments:
ID [required]
Options:
--age INTEGER RANGE [default: 20]
--score FLOAT RANGE [default: 0]
--help Show this message and exit.
// Pass all the CLI parameters
$ python main.py 5 --age 20 --score 90
ID is 5
--age is 20
--score is 90.0
// Pass an invalid ID
$ python main.py 1002
Usage: main.py [OPTIONS] ID
Try "main.py --help" for help.
Error: Invalid value for 'ID': 1002 is not in the range 0<=x<=1000.
// Pass an invalid age
$ python main.py 5 --age 15
Usage: main.py [OPTIONS] ID
Try "main.py --help" for help.
Error: Invalid value for '--age': 15 is not in the range x>=18.
// Pass an invalid score
$ python main.py 5 --age 20 --score 100.5
Usage: main.py [OPTIONS] ID
Try "main.py --help" for help.
Error: Invalid value for '--score': 100.5 is not in the range x<=100.
// But as we didn't specify a minimum score, this is accepted
$ python main.py 5 --age 20 --score -5
ID is 5
--age is 20
--score is -5.0
```
## Clamping numbers
You might want to, instead of showing an error, use the closest minimum or maximum valid values.
You can do it with the `clamp` parameter:
{* docs_src/parameter_types/number/tutorial002_an_py310.py hl[10:12] *}
And then, when you pass data that is out of the valid range, it will be "clamped", the closest valid value will be used:
```console
// ID doesn't have clamp, so it shows an error
$ python main.py 1002
Usage: main.py [OPTIONS] ID
Try "main.py --help" for help.
Error: Invalid value for 'ID': 1002 is not in the range 0<=x<=1000.
// But --rank and --score use clamp
$ python main.py 5 --rank 11 --score -5
ID is 5
--rank is 10
--score is 0
```
## Counter *CLI options*
You can make a *CLI option* work as a counter with the `count` parameter:
{* docs_src/parameter_types/number/tutorial003_an_py310.py hl[9] *}
It means that the *CLI option* will be like a boolean flag, e.g. `--verbose`.
And the value you receive in the function will be the amount of times that `--verbose` was added:
```console
// Check it
$ python main.py
Verbose level is 0
// Now use one --verbose
$ python main.py --verbose
Verbose level is 1
// Now 3 --verbose
$ python main.py --verbose --verbose --verbose
Verbose level is 3
// And with the short name
$ python main.py -v
Verbose level is 1
// And with the short name 3 times
$ python main.py -v -v -v
Verbose level is 3
// As short names can be put together, this also works
$ python main.py -vvv
Verbose level is 3
```
================================================
FILE: docs/tutorial/parameter-types/path.md
================================================
# Path
You can declare a *CLI parameter* to be a standard Python `pathlib.Path`.
This is what you would do for directory paths, file paths, etc:
{* docs_src/parameter_types/path/tutorial001_an_py310.py hl[1,10] *}
And again, as you receive a standard Python `Path` object the same as the type annotation, your editor will give you autocompletion for all its attributes and methods.
Check it:
```console
// No config
$ python main.py
No config file
Aborted!
// Pass a config that doesn't exist
$ python main.py --config config.txt
The config doesn't exist
// Now create a quick config
$ echo "some settings" > config.txt
// And try again
$ python main.py --config config.txt
Config file contents: some settings
// And with a directory
$ python main.py --config ./
Config is a directory, will use all its config files
```
## Path validations
You can perform several validations for `Path` *CLI parameters*:
* `exists`: if set to true, the file or directory needs to exist for this value to be valid. If this is not required and a file does indeed not exist, then all further checks are silently skipped.
* `file_okay`: controls if a file is a possible value.
* `dir_okay`: controls if a directory is a possible value.
* `writable`: if true, a writable check is performed.
* `readable`: if true, a readable check is performed.
* `resolve_path`: if this is true, then the path is fully resolved before the value is passed onwards. This means that it’s absolute and symlinks are resolved.
/// note | Technical Details
It will not expand a tilde-prefix (something with `~`, like `~/Documents/`), as this is supposed to be done by the shell only.
///
For example:
{* docs_src/parameter_types/path/tutorial002_an_py310.py hl[14:19] *}
Check it:
```console
$ python main.py --config config.txt
Usage: main.py [OPTIONS]
Try "main.py --help" for help.
Error: Invalid value for '--config': File 'config.txt' does not exist.
// Now create a quick config
$ echo "some settings" > config.txt
// And try again
$ python main.py --config config.txt
Config file contents: some settings
// And with a directory
$ python main.py --config ./
Usage: main.py [OPTIONS]
Try "main.py --help" for help.
Error: Invalid value for '--config': File './' is a directory.
```
### Advanced `Path` configurations
/// warning | Advanced Details
You probably won't need these configurations at first, you may want to skip it.
They are used for more advanced use cases.
///
* `allow_dash`: If this is set to True, a single dash to indicate standard streams is permitted.
* `path_type`: optionally a string type that should be used to represent the path. The default is `None` which means the return value will be either bytes or unicode depending on what makes most sense given the input data.
================================================
FILE: docs/tutorial/parameter-types/uuid.md
================================================
# UUID
/// info
A UUID is a "Universally Unique Identifier".
It's a standard format for identifiers, like passport numbers, but for anything, not just people in countries.
They look like this:
```
d48edaa6-871a-4082-a196-4daab372d4a1
```
The way they are generated makes them sufficiently long and random that you could assume that every UUID generated is unique. Even if it was generated by a different application, database, or system.
So, if your system uses UUIDs to identify your data, you could mix it with the data from some other system that also uses UUIDs with some confidence that their IDs (UUIDs) won't clash with yours.
This wouldn't be true if you just used `int`s as identifiers, as most databases do.
///
You can declare a *CLI parameter* as a UUID:
{* docs_src/parameter_types/uuid/tutorial001_py310.py hl[1,9:11] *}
Your Python code will receive a standard Python `UUID` object with all its attributes and methods, and as you are annotating your function parameter with that type, you will have type checks, autocompletion in your editor, etc.
Check it:
```console
// Pass a valid UUID v4
$ python main.py d48edaa6-871a-4082-a196-4daab372d4a1
USER_ID is d48edaa6-871a-4082-a196-4daab372d4a1
UUID version is: 4
// An invalid value
$ python main.py 7479706572-72756c6573
Usage: main.py [OPTIONS] USER_ID
Try "main.py --help" for help.
Error: Invalid value for 'USER_ID': 7479706572-72756c6573 is not a valid UUID.
```
================================================
FILE: docs/tutorial/printing.md
================================================
# Printing and Colors
You can use the normal `print()` to show information on the screen:
{* docs_src/typer_app/tutorial001_py310.py hl[8] *}
It will show the output normally:
```console
$ python main.py World
Hello World
```
## Use Rich
You can also display beautiful and more complex information using Rich. It comes by default when you install `typer`.
### Use Rich `print`
For the simplest cases, you can just import `print` from `rich` and use it instead of the standard `print`:
{* docs_src/printing/tutorial001_py310.py hl[2,18] *}
Just with that, **Rich** will be able to print your data with nice colors and structure:
```console
$ python main.py
Here's the data
{
'name': 'Rick',
'age': 42,
'items': [
{'name': 'Portal Gun'},
{'name': 'Plumbus'}
],
'active': True,
'affiliation': None
}
```
### Rich Markup
Rich also supports a custom markup syntax to set colors and styles, for example:
{* docs_src/printing/tutorial002_py310.py hl[9] *}
```console
$ python main.py
Alert! Portal gun shooting! 💥
```
In this example you can see how to use font styles, colors, and even emojis.
To learn more check out the Rich docs.
### Rich Tables
The way Rich works internally is that it uses a `Console` object to display the information.
When you call Rich's `print`, it automatically creates this object and uses it.
But for advanced use cases, you could create a `Console` yourself.
{* docs_src/printing/tutorial003_py310.py hl[2:3,5,12:15] *}
In this example, we create a `Console`, and a `Table`. And then we can add some rows to the table, and print it.
If you run it, you will see a nicely formatted table:
```console
$ python main.py
┏━━━━━━━┳━━━━━━━━━━━━┓
┃ Name ┃ Item ┃
┡━━━━━━━╇━━━━━━━━━━━━┩
│ Rick │ Portal Gun │
│ Morty │ Plumbus │
└───────┴────────────┘
```
Rich has many other features, as an example, you can check the docs for:
* Prompt
* Markdown
* Panel
* ...and more.
### Typer and Rich
If you are wondering what tool should be used for what, **Typer** is useful for structuring the command line application, with options, arguments, subcommands, data validation, etc.
In general, **Typer** tends to be the entry point to your program, taking the first input from the user.
**Rich** is useful for the parts that need to *display* information. Showing beautiful content on the screen.
The best results for your command line application are achieved combining both **Typer** and **Rich**.
## "Standard Output" and "Standard Error"
The way printing works underneath is that the **operating system** (Linux, Windows, macOS) treats what we print as if our CLI program was **writing text** to a "**virtual file**" called "**standard output**".
When our code "prints" things it is actually "writing" to this "virtual file" of "standard output".
This might seem strange, but that's how the CLI program and the operating system interact with each other.
And then the operating system **shows on the screen** whatever our CLI program "**wrote**" to that "**virtual file**" called "**standard output**".
### Standard Error
And there's another "**virtual file**" called "**standard error**" that is normally only used for errors.
But we can also "print" to "standard error". And both are shown on the terminal to the users.
/// info
If you use PowerShell it's quite possible that what you print to "standard error" won't be shown in the terminal.
In PowerShell, to see "standard error" you would have to check the variable `$Error`.
But it will work normally in Bash, Zsh, and Fish.
///
### Printing to "standard error"
You can print to "standard error" creating a Rich `Console` with `stderr=True`.
/// tip
`stderr` is short for "standard error".
///
Using `stderr=True` tells **Rich** that the output should be shown in "standard error".
{* docs_src/printing/tutorial004_py310.py hl[4,11] *}
When you try it in the terminal, it will probably just look the same:
```console
$ python main.py
Here is something written to standard error
```
## "Standard Input"
As a final detail, when you type text in your keyboard to your terminal, the operating system also considers it another "**virtual file**" that you are writing text to.
This virtual file is called "**standard input**".
### What is this for
Right now this probably seems quite useless 🤷♂.
But understanding that will come handy in the future, for example for autocompletion and testing.
## Typer Echo
/// warning
In most of the cases, for displaying advanced information, it is recommended to use Rich.
You can probably skip the rest of this section. 🎉😎
///
**Typer** also has a small utility `typer.echo()` to print information on the screen. But normally you shouldn't need it.
For the simplest cases, you can use the standard Python `print()`.
And for the cases where you want to display data more beautifully, or more advanced content, you should use **Rich** instead.
### Why `typer.echo`
`typer.echo()` (which is actually just `click.echo()`) applies some checks to try and convert binary data to strings, and other similar things.
But in most of the cases you wouldn't need it, as in modern Python strings (`str`) already support and use Unicode, and you would rarely deal with pure `bytes` that you want to print on the screen.
If you have some `bytes` objects, you would probably want to decode them intentionally and directly before trying to print them.
And if you want to print data with colors and other features, you are much better off with the more advanced tools in **Rich**.
### Color
/// note | Technical Details
The way color works in terminals is by using some codes (ANSI escape sequences) as part of the text.
So, a colored text is still just a `str`.
///
/// tip
Again, you are much better off using Rich for this. 😎
///
You can create colored strings to output to the terminal with `typer.style()`, that gives you `str`s that you can then pass to `typer.echo()`:
{* docs_src/printing/tutorial005_py310.py hl[10,12] *}
/// tip
The parameters `fg` and `bg` receive strings with the color names for the "foreground" and "background" colors. You could simply pass `fg="green"` and `bg="red"`.
But **Typer** provides them all as variables like `typer.colors.GREEN` just so you can use autocompletion while selecting them.
///
Check it:
python main.py
everything is good
python main.py --no-good
everything is bad
You can pass these function arguments to `typer.style()`:
* `fg`: the foreground color.
* `bg`: the background color.
* `bold`: enable or disable bold mode.
* `dim`: enable or disable dim mode. This is badly supported.
* `underline`: enable or disable underline.
* `blink`: enable or disable blinking.
* `reverse`: enable or disable inverse rendering (foreground becomes background and the other way round).
* `reset`: by default a reset-all code is added at the end of the string which means that styles do not carry over. This can be disabled to compose styles.
### `typer.secho()` - style and print
/// tip
In case you didn't see above, you are much better off using Rich for this. 😎
///
There's a shorter form to style and print at the same time with `typer.secho()` it's like `typer.echo()` but also adds style like `typer.style()`:
{* docs_src/printing/tutorial006_py310.py hl[8] *}
Check it:
python main.py Camila
Welcome here Camila
================================================
FILE: docs/tutorial/progressbar.md
================================================
# Progress Bar
If you are executing an operation that can take some time, you can inform it to the user. 🤓
## Progress Bar
You can use Rich's Progress Display to show a progress bar, for example:
{* docs_src/progressbar/tutorial001_py310.py hl[4,12] *}
You put the thing that you want to iterate over inside of Rich's `track()`, and then iterate over that.
Check it:
```console
$ python main.py
---> 100%
Processed 100 things.
```
...actually, it will look a lot prettier. ✨ But I can't show you the animation here in the docs. 😅
The colors and information will look something like this:
```console
$ python main.py
Processing... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸━━━━━━━━━━ 74% 0:00:01
```
## Spinner
When you don't know how long the operation will take, you can use a spinner instead.
Rich allows you to display many things in complex and advanced ways.
For example, this will show two spinners:
{* docs_src/progressbar/tutorial002_py310.py hl[4,11:18] *}
I can't show you the beautiful animation here in the docs. 😅
But at some point in time it will look like this (imagine it's spinning). 🤓
```console
$ python main.py
⠹ Processing...
⠹ Preparing...
```
You can learn more about it in the Rich docs for Progress Display.
## Typer `progressbar`
If you can, you should use **Rich** as explained above, it has more features, it's more advanced, and can display information more beautifully. ✨
/// tip
If you can use Rich, use the information above, the Rich docs, and skip the rest of this page. 😎
///
But if you can't use Rich and have it disabled, Typer comes with a simple utility to show progress bars.
### Use `typer.progressbar`
/// tip
Remember, you are much better off using Rich for this. 😎
///
You can use `typer.progressbar()` with a `with` statement, as in:
```Python
with typer.progressbar(something) as progress:
pass
```
And you pass as function argument to `typer.progressbar()` the thing that you would normally iterate over.
{* docs_src/progressbar/tutorial003_py310.py hl[11] *}
So, if you have a list of users, this could be:
```Python
users = ["Camila", "Rick", "Morty"]
with typer.progressbar(users) as progress:
pass
```
And the `with` statement using `typer.progressbar()` gives you an object that you can iterate over, just like if it was the same thing that you would iterate over normally.
But by iterating over this object **Typer** will know to update the progress bar:
```Python
users = ["Camila", "Rick", "Morty"]
with typer.progressbar(users) as progress:
for user in progress:
typer.echo(user)
```
/// tip
Notice that there are 2 levels of code blocks. One for the `with` statement and one for the `for` statement.
///
/// info
This is mostly useful for operations that take some time.
In the example above we are faking it with `time.sleep()`.
///
Check it:
```console
$ python main.py
---> 100%
Processed 100 things.
```
### Setting a Progress Bar `length`
/// tip
Remember, you are much better off using Rich for this. 😎
///
The progress bar is generated from the length of the iterable (e.g. the list of users).
But if the length is not available (for example, with something that fetches a new user from a web API each time) you can pass an explicit `length` to `typer.progressbar()`.
{* docs_src/progressbar/tutorial004_py310.py hl[18] *}
Check it:
```console
$ python main.py
---> 100%
Processed 100 user IDs.
```
#### About the function with `yield`
If you hadn't seen something like that `yield` above, that's a "generator".
You can iterate over that function with a `for` and at each iteration it will give you the value at `yield`.
`yield` is like a `return` that gives values multiple times and let's you use the function in a `for` loop.
For example:
```Python
def iterate_user_ids():
# Let's imagine this is a web API, not a range()
for i in range(100):
yield i
for i in iterate_user_ids():
print(i)
```
would print each of the "user IDs" (here it's just the numbers from `0` to `99`).
### Add a `label`
/// tip
Remember, you are much better off using Rich for this. 😎
///
You can also set a `label`:
{* docs_src/progressbar/tutorial005_py310.py hl[11] *}
Check it:
python main.py
Processed 100 things.
## Iterate manually
If you need to manually iterate over something and update the progress bar irregularly, you can do it by not passing an iterable but just a `length` to `typer.progressbar()`.
And then calling the `.update()` method in the object from the `with` statement:
{* docs_src/progressbar/tutorial006_py310.py hl[11,17] *}
Check it:
python main.py
Processed 1000 things in batches.
================================================
FILE: docs/tutorial/prompt.md
================================================
# Ask with Prompt
When you need to ask the user for info interactively you should normally use [*CLI Option*s with Prompt](options/prompt.md){.internal-link target=_blank}, because they allow using the CLI program in a non-interactive way (for example, a Bash script could use it).
But if you absolutely need to ask for interactive information without using a *CLI option*, you can use `typer.prompt()`:
{* docs_src/prompt/tutorial001_py310.py hl[8] *}
Check it:
```console
$ python main.py
# What's your name?:$ Camila
Hello Camila
```
## Confirm
There's also an alternative to ask for confirmation. Again, if possible, you should use a [*CLI Option* with a confirmation prompt](options/prompt.md){.internal-link target=_blank}:
{* docs_src/prompt/tutorial002_py310.py hl[8] *}
Check it:
```console
$ python main.py
# Are you sure you want to delete it? [y/N]:$ y
Deleting it!
// This time cancel it
$ python main.py
# Are you sure you want to delete it? [y/N]:$ n
Not deleting
Aborted!
```
## Confirm or abort
As it's very common to abort if the user doesn't confirm, there's an integrated parameter `abort` that does it automatically:
{* docs_src/prompt/tutorial003_py310.py hl[8] *}
```console
$ python main.py
# Are you sure you want to delete it? [y/N]:$ y
Deleting it!
// This time cancel it
$ python main.py
# Are you sure you want to delete it? [y/N]:$ n
Aborted!
```
## Prompt with Rich
You can use Rich to prompt the user for input:
{* docs_src/prompt/tutorial004_py310.py hl[2,9] *}
And when you run it, it will look like:
```console
$ python main.py
# Enter your name 😎:$ Morty
Hello Morty
```
================================================
FILE: docs/tutorial/subcommands/add-typer.md
================================================
# Add Typer
We'll start with the core idea.
To add a `typer.Typer()` app inside of another.
## Manage items
Let's imagine that you are creating a *CLI program* to manage items in some distant land.
It could be in an `items.py` file with this:
{* docs_src/subcommands/tutorial001_py310/items.py *}
And you would use it like:
```console
$ python items.py create Wand
Creating item: Wand
```
## Manage users
But then you realize that you also have to manage users from your *CLI app*.
It could be a file `users.py` with something like:
{* docs_src/subcommands/tutorial001_py310/users.py *}
And you would use it like:
```console
$ python users.py create Camila
Creating user: Camila
```
## Put them together
Both parts are similar. In fact, `items.py` and `users.py` both have commands `create` and `delete`.
But we need them to be part of the same *CLI program*.
In this case, as with `git remote`, we can put them together as subcommands in another `typer.Typer()` *CLI program*.
Now create a `main.py` with:
{* docs_src/subcommands/tutorial001_py310/main.py hl[3,4,7,8] *}
Here's what we do in `main.py`:
* Import the other Python modules (the files `users.py` and `items.py`).
* Create the main `typer.Typer()` application.
* Use `app.add_typer()` to include the `app` from `items.py` and `users.py`, each of those 2 was also created with `typer.Typer()`.
* Define a `name` with the command that will be used for each of these "sub-Typers" to group their own commands.
And now your *CLI program* has 2 commands:
* `users`: with all of the commands (subcommands) in the `app` from `users.py`.
* `items` with all the commands (subcommands) in the `app` from `items.py`.
Check it:
```console
// Check the help
$ python main.py --help
Usage: main.py [OPTIONS] COMMAND [ARGS]...
Options:
--install-completion Install completion for the current shell.
--show-completion Show completion for the current shell, to copy it or customize the installation.
--help Show this message and exit.
Commands:
items
users
```
Now you have a *CLI program* with commands `items` and `users`, and they in turn have their own commands (subcommands).
Let's check the `items` command:
```console
// Check the help for items
$ python main.py items --help
// It shows its own commands (subcommands): create, delete, sell
Usage: main.py items [OPTIONS] COMMAND [ARGS]...
Options:
--help Show this message and exit.
Commands:
create
delete
sell
// Try it
$ python main.py items create Wand
Creating item: Wand
$ python main.py items sell Vase
Selling item: Vase
```
/// tip
Notice that we are still calling `$ python main.py` but now we are using the command `items`.
///
And now check the command `users`, with all its subcommands:
```console
$ python main.py users --help
Usage: main.py users [OPTIONS] COMMAND [ARGS]...
Options:
--help Show this message and exit.
Commands:
create
delete
// Try it
$ python main.py users create Camila
Creating user: Camila
```
## Recap
That's the core idea.
You can just create `typer.Typer()` apps and add them inside one another.
And you can do that with any levels of commands that you want.
Do you need sub-sub-sub-subcommands? Go ahead, create all the `typer.Typer()`s you need and put them together with `app.add_typer()`.
In the next sections we'll update this with more features, but you already have the core idea.
This way, **Typer** applications are composable, each `typer.Typer()` can be a *CLI app* by itself, but it can also be added as a command group to another Typer app.
================================================
FILE: docs/tutorial/subcommands/callback-override.md
================================================
# Sub-Typer Callback Override
When creating a **Typer** app you can define a callback function, it always executes and defines the *CLI arguments* and *CLI options* that go before a command.
When adding a Typer app inside of another, the sub-Typer can also have its own callback.
It can handle any *CLI parameters* that go before its own commands and execute any extra code:
{* docs_src/subcommands/callback_override/tutorial001_py310.py hl[9,10,11] *}
In this case it doesn't define any *CLI parameters*, it just writes a message.
Check it:
```console
$ python main.py users create Camila
// Notice the first message is not created by the command function but by the callback
Running a users command
Creating user: Camila
```
## Add a callback on creation
It's also possible to add a callback when creating the `typer.Typer()` app that will be added to another Typer app:
{* docs_src/subcommands/callback_override/tutorial002_py310.py hl[6,7,10] *}
This achieves exactly the same as above, it's just another place to add the callback.
Check it:
```console
$ python main.py users create Camila
Running a users command
Creating user: Camila
```
## Overriding the callback on creation
If a callback was added when creating the `typer.Typer()` app, it's possible to override it with a new one using `@app.callback()`.
This is the same information you saw on the section about [Commands - Typer Callback](../commands/callback.md){.internal-link target=_blank}, and it applies the same for sub-Typer apps:
{* docs_src/subcommands/callback_override/tutorial003_py310.py hl[6,7,10,14,15,16] *}
Here we had defined a callback when creating the `typer.Typer()` sub-app, but then we override it with a new callback with the function `user_callback()`.
As `@app.callback()` takes precedence over `typer.Typer(callback=some_function)`, now our CLI app will use this new callback.
Check it:
```console
$ python main.py users create Camila
// Notice the message from the new callback
Callback override, running users command
Creating user: Camila
```
## Overriding the callback when adding a sub-Typer
Lastly, you can override the callback defined anywhere else when adding a sub-Typer with `app.add_typer()` using the `callback` parameter.
This has the highest priority:
{* docs_src/subcommands/callback_override/tutorial004_py310.py hl[13,14,17] *}
Notice that the precedence goes to `app.add_typer()` and is not affected by the order of execution. There's another callback defined below, but the one from `app.add_typer()` wins.
Now when you use the CLI program it will use the new callback function `callback_for_add_typer()`.
Check it:
```console
$ python users create Camila
// Notice the message from the callback added in add_typer()
I have the high land! Running users command
Creating user: Camila
```
================================================
FILE: docs/tutorial/subcommands/index.md
================================================
# SubCommands - Command Groups
You read before how to create a program with [Commands](../commands/index.md){.internal-link target=_blank}.
Now we'll see how to create a *CLI program* with commands that have their own subcommands. Also known as command groups.
For example, the *CLI program* `git` has a command `remote`.
But `git remote`, in turn, has its own subcommands, like `add`:
```console
// git remote alone shows the current remote repositories
$ git remote
origin
// Use -v to make it verbose and show more info
$ git remote -v
origin git@github.com:yourusername/typer.git (fetch)
origin git@github.com:yourusername/typer.git (push)
// git remote add takes 2 CLI arguments, a name and URL
$ git remote add upstream https://github.com/fastapi/typer.git
// Doesn't output anything, but now you have another remote repository called upstream
// Now check again
$ git remote -v
origin git@github.com:yourusername/typer.git (fetch)
origin git@github.com:yourusername/typer.git (push)
upstream https://github.com/fastapi/typer.git (fetch)
upstream https://github.com/fastapi/typer.git (push)
```
In the next sections we'll see how to create subcommands like these.
================================================
FILE: docs/tutorial/subcommands/name-and-help.md
================================================
# SubCommand Name and Help
When adding a Typer app to another we have seen how to set the `name` to use for the command.
For example to set the command to `users`:
```Python
app.add_typer(users.app, name="users")
```
## Add a help text
We can also set the `help` text while adding a Typer:
{* docs_src/subcommands/name_help/tutorial001_py310.py hl[6] *}
And then we get that help text for that command in the *CLI program*:
```console
// Check the main help
$ python main.py --help
Usage: main.py [OPTIONS] COMMAND [ARGS]...
Options:
--install-completion Install completion for the current shell.
--show-completion Show completion for the current shell, to copy it or customize the installation.
--help Show this message and exit.
Commands:
users Manage users in the app.
// Check the help for the users command
$ python main.py users --help
Usage: main.py users [OPTIONS] COMMAND [ARGS]...
Manage users in the app.
Options:
--help Show this message and exit.
Commands:
create
```
We can set the `help` in several places, each one taking precedence over the other, overriding the previous value.
Let's see those locations.
/// tip
There are other attributes that can be set in that same way in the same places we'll see next.
But those are documented later in another section.
///
## Inferring help text from callback
### Inferring a command's help text
When you create a command with `@app.command()`, by default, it generates the name from the function name.
And by default, the help text is extracted from the function's docstring.
For example:
```Python
@app.command()
def create(item: str):
"""
Create an item.
"""
typer.echo(f"Creating item: {item}")
```
...will create a command `create` with a help text of `Create an item`.
### Inferring the help text from `@app.callback()`
The same way, if you define a callback in a `typer.Typer()`, the help text is extracted from the callback function's docstring.
Here's an example:
{* docs_src/subcommands/name_help/tutorial002_py310.py hl[9,10,11,12,13] *}
The help text for that command will be the callback function's docstring: `Manage users in the app.`.
Check it:
```console
// Check the main help
$ python main.py --help
// Notice the help text "Manage users in the app."
Usage: main.py [OPTIONS] COMMAND [ARGS]...
Options:
--install-completion Install completion for the current shell.
--show-completion Show completion for the current shell, to copy it or customize the installation.
--help Show this message and exit.
Commands:
users Manage users in the app.
// Check the help for the users command
$ python main.py users --help
// Notice the main description: "Manage users in the app."
Usage: main.py users [OPTIONS] COMMAND [ARGS]...
Manage users in the app.
Options:
--help Show this message and exit.
Commands:
create
```
/// note
Before Typer 0.14.0, in addition to the help text, the command name was also inferred from the callback function name, this is no longer the case.
///
### Help from callback parameter in `typer.Typer()`
If you pass a `callback` parameter while creating a `typer.Typer(callback=some_function)` it will be used to infer the help text.
This has the lowest priority, we'll see later what has a higher priority and can override it.
Check the code:
{* docs_src/subcommands/name_help/tutorial003_py310.py hl[6,7,8,9,12] *}
This achieves exactly the same as the previous example.
Check it:
```console
// Check the main help
$ python main.py --help
// Notice the help text "Manage users in the app."
Usage: main.py [OPTIONS] COMMAND [ARGS]...
Options:
--install-completion Install completion for the current shell.
--show-completion Show completion for the current shell, to copy it or customize the installation.
--help Show this message and exit.
Commands:
users Manage users in the app.
// Check the help for the users command
$ python main.py users --help
// Notice the main description: "Manage users in the app."
Usage: main.py users [OPTIONS] COMMAND [ARGS]...
Manage users in the app.
Options:
--help Show this message and exit.
Commands:
create
```
### Override a callback set in `typer.Typer()` with `@app.callback()`
The same as with normal **Typer** apps, if you pass a `callback` to `typer.Typer(callback=some_function)` and then override it with `@app.callback()`, the help text will be inferred from the new callback:
{* docs_src/subcommands/name_help/tutorial004_py310.py hl[16,17,18,19,20] *}
Now the help text will be `Manage users in the app.` instead of `Old callback help.`.
Check it:
```console
// Check the main help
$ python main.py --help
// Notice the help text "Manage users in the app."
Usage: main.py [OPTIONS] COMMAND [ARGS]...
Options:
--install-completion Install completion for the current shell.
--show-completion Show completion for the current shell, to copy it or customize the installation.
--help Show this message and exit.
Commands:
users Manage users in the app.
// Check the help for the users command
$ python main.py users --help
// Notice the main description: "Manage users in the app."
Usage: main.py users [OPTIONS] COMMAND [ARGS]...
Manage users in the app.
Options:
--help Show this message and exit.
Commands:
create
```
### Help from callback in `app.add_typer()`
If you override the callback in `app.add_typer()` when including a sub-app, the help will be inferred from this callback function.
This takes precedence over inferring the help from a callback set in `@sub_app.callback()` and `typer.Typer(callback=sub_app_callback)`.
Check the code:
{* docs_src/subcommands/name_help/tutorial005_py310.py hl[15,16,17,18,21] *}
The help text will be `I have the highland! Create some users.` instead of the previous ones.
Check it:
```console
// Check the main help
$ python main.py --help
// Check the command new-users and its help text
Usage: main.py [OPTIONS] COMMAND [ARGS]...
Options:
--install-completion Install completion for the current shell.
--show-completion Show completion for the current shell, to copy it or customize the installation.
--help Show this message and exit.
Commands:
new-users I have the highland! Create some users.
// Now check the help for the new-users command
$ python main.py new-users --help
// Notice the help text
Usage: main.py new-users [OPTIONS] COMMAND [ARGS]...
I have the highland! Create some users.
Options:
--help Show this message and exit.
Commands:
create
```
### Enough inferring
So, when inferring help text, the precedence order from lowest priority to highest is:
* `sub_app = typer.Typer(callback=some_function)`
* `@sub_app.callback()`
* `app.add_typer(sub_app, callback=new_function)`
That's for inferring the help text from functions.
But if you set the help text explicitly, that has a higher priority than these.
## Set the name and help
Let's now see the places where you can set the command name and help text, from lowest priority to highest.
/// tip
Setting the help text explicitly always has a higher precedence than inferring from a callback function.
///
### Name and help in `typer.Typer()`
You could have all the callbacks and overrides we defined before, but the help text was inferred from the function docstring.
If you set it explicitly, that takes precedence over inferring.
You can set it when creating a new `typer.Typer()`:
{* docs_src/subcommands/name_help/tutorial006_py310.py hl[12] *}
/// info
The rest of the callbacks and overrides are there only to show you that they don't affect the name and help text when you set it explicitly.
///
We set an explicit help `Explicit help.`.
So that will take precedence now.
Check it:
```console
// Check the main help
$ python main.py --help
// Notice the command name is exp-users and the help text is "Explicit help."
Usage: main.py [OPTIONS] COMMAND [ARGS]...
Options:
--install-completion Install completion for the current shell.
--show-completion Show completion for the current shell, to copy it or customize the installation.
--help Show this message and exit.
Commands:
exp-users Explicit help.
// Check the help for the exp-users command
$ python main.py exp-users --help
// Notice the main help text
Usage: main.py exp-users [OPTIONS] COMMAND [ARGS]...
Explicit help.
Options:
--help Show this message and exit.
Commands:
create
```
### Help text in `@app.callback()`
Many parameters that you use when creating a `typer.Typer()` app can be overridden in the parameters of `@app.callback()`.
Continuing with the previous example, we now override the `help` in `@user_app.callback()`:
{* docs_src/subcommands/name_help/tutorial007_py310.py hl[24] *}
And now the help text will be `Help from callback for users.`.
Check it:
```console
// Check the help
$ python main.py --help
// The help text is now "Help from callback for users.".
Usage: main.py [OPTIONS] COMMAND [ARGS]...
Options:
--install-completion Install completion for the current shell.
--show-completion Show completion for the current shell, to copy it or customize the installation.
--help Show this message and exit.
Commands:
users Help from callback for users.
// Check the users command help
$ python main.py users --help
// Notice the main help text
Usage: main.py users [OPTIONS] COMMAND [ARGS]...
Help from callback for users.
Options:
--help Show this message and exit.
Commands:
create
```
### Name and help in `app.add_typer()`
And finally, with the highest priority, you can override all that by explicitly setting the `name` and `help` in `app.add_typer()`, just like we did on the first example above:
{* docs_src/subcommands/name_help/tutorial008_py310.py hl[21] *}
And now, with the highest priorities of them all, the command name will now be `cake-sith-users` and the help text will be `Unlimited powder! Eh, users.`.
Check it:
```console
// Check the help
$ python main.py --help
// Notice the command name cake-sith-users and the new help text "Unlimited powder! Eh, users."
Usage: main.py [OPTIONS] COMMAND [ARGS]...
Options:
--install-completion Install completion for the current shell.
--show-completion Show completion for the current shell, to copy it or customize the installation.
--help Show this message and exit.
Commands:
cake-sith-users Unlimited powder! Eh, users.
// And check the help for the command cake-sith-users
$ python main.py cake-sith-users --help
// Notice the main help text
Usage: main.py cake-sith-users [OPTIONS] COMMAND [ARGS]...
Unlimited powder! Eh, users.
Options:
--help Show this message and exit.
Commands:
create
```
## Recap
The precedence to generate a command's **help**, from lowest priority to highest, is:
* Implicitly inferred from `sub_app = typer.Typer(callback=some_function)`
* Implicitly inferred from the callback function under `@sub_app.callback()`
* Implicitly inferred from `app.add_typer(sub_app, callback=some_function)`
* Explicitly set on `sub_app = typer.Typer(help="Some help.")`
* Explicitly set on `app.add_typer(sub_app, help="Some help.")`
And the priority to set the command's **name**, from lowest priority to highest, is:
* Explicitly set on `sub_app = typer.Typer(name="some-name")`
* Explicitly set on `app.add_typer(sub_app, name="some-name")`
So, `app.add_typer(sub_app, name="some-name", help="Some help.")` always wins.
================================================
FILE: docs/tutorial/subcommands/nested-subcommands.md
================================================
# Nested SubCommands
We'll now see how these same ideas can be extended for deeply nested commands.
Let's imagine that the same *CLI program* from the previous examples now needs to handle `lands`.
But a land could be a `reign` or `town`.
And each of those could have their own commands, like `create` and `delete`.
## A CLI app for reigns
Let's start with a file `reigns.py`:
{* docs_src/subcommands/tutorial003_py310/reigns.py *}
This is already a simple *CLI program* to manage reigns:
```console
// Check the help
$ python reigns.py --help
Usage: reigns.py [OPTIONS] COMMAND [ARGS]...
Options:
--install-completion Install completion for the current shell.
--show-completion Show completion for the current shell, to copy it or customize the installation.
--help Show this message and exit.
Commands:
conquer
destroy
// Try it
$ python reigns.py conquer Cintra
Conquering reign: Cintra
$ python reigns.py destroy Mordor
Destroying reign: Mordor
```
## A CLI app for towns
And now the equivalent for managing towns in `towns.py`:
{* docs_src/subcommands/tutorial003_py310/towns.py *}
With it, you can manage towns:
```console
// Check the help
$ python towns.py --help
Usage: towns.py [OPTIONS] COMMAND [ARGS]...
Options:
--install-completion Install completion for the current shell.
--show-completion Show completion for the current shell, to copy it or customize the installation.
--help Show this message and exit.
Commands:
burn
found
// Try it
$ python towns.py found "New Asgard"
Founding town: New Asgard
$ python towns.py burn Vizima
Burning town: Vizima
```
## Manage the land in a CLI app
Now let's put the `reigns` and `towns` together in the same *CLI program* in `lands.py`:
{* docs_src/subcommands/tutorial003_py310/lands.py *}
And now we have a single *CLI program* with a command (or command group) `reigns` that has its own commands. And another command `towns` with its own subcommands.
Check it:
```console
// Check the help
$ python lands.py --help
Usage: lands.py [OPTIONS] COMMAND [ARGS]...
Options:
--install-completion Install completion for the current shell.
--show-completion Show completion for the current shell, to copy it or customize the installation.
--help Show this message and exit.
Commands:
reigns
towns
// We still have the help for reigns
$ python lands.py reigns --help
Usage: lands.py reigns [OPTIONS] COMMAND [ARGS]...
Options:
--help Show this message and exit.
Commands:
conquer
destroy
// And the help for towns
$ python lands.py towns --help
Usage: lands.py towns [OPTIONS] COMMAND [ARGS]...
Options:
--help Show this message and exit.
Commands:
burn
found
```
Now try it, manage the lands through the CLI:
```console
// Try the reigns command
$ python lands.py reigns conquer Gondor
Conquering reign: Gondor
$ python lands.py reigns destroy Nilfgaard
Destroying reign: Nilfgaard
// Try the towns command
$ python lands.py towns found Springfield
Founding town: Springfield
$ python lands.py towns burn Atlantis
Burning town: Atlantis
```
## Deeply nested subcommands
Now let's say that all these commands in the `lands.py` *CLI program* should be part of the previous *CLI program* we built in the first example.
We want our *CLI program* to have these commands/command groups:
* `users`:
* `create`
* `delete`
* `items`:
* `create`
* `delete`
* `sell`
* `lands`:
* `reigns`:
* `conquer`
* `destroy`
* `towns`:
* `found`
* `burn`
This already is a quite deeply nested "tree" of commands/command groups.
But to achieve that, we just have to add the `lands` **Typer** app to the same `main.py` file we already had:
{* docs_src/subcommands/tutorial003_py310/main.py hl[4,10] *}
And now we have everything in a single *CLI program*:
```console
// Check the main help
$ python main.py --help
Usage: main.py [OPTIONS] COMMAND [ARGS]...
Options:
--install-completion Install completion for the current shell.
--show-completion Show completion for the current shell, to copy it or customize the installation.
--help Show this message and exit.
Commands:
items
lands
users
// Try some users commands
$ python main.py users create Camila
Creating user: Camila
// Now try some items commands
$ python main.py items create Sword
Creating item: Sword
// And now some lands commands for reigns
$ python main.py lands reigns conquer Gondor
Conquering reign: Gondor
// And for towns
$ python main.py lands towns found Cartagena
Founding town: Cartagena
```
## Review the files
Here are all the files if you want to review/copy them:
`reigns.py`:
{* docs_src/subcommands/tutorial003_py310/reigns.py *}
`towns.py`:
{* docs_src/subcommands/tutorial003_py310/towns.py *}
`lands.py`:
{* docs_src/subcommands/tutorial003_py310/lands.py *}
`users.py`:
{* docs_src/subcommands/tutorial003_py310/users.py *}
`items.py`:
{* docs_src/subcommands/tutorial003_py310/items.py *}
`main.py`:
{* docs_src/subcommands/tutorial003_py310/main.py *}
/// tip
All these files have an `if __name__ == "__main__"` block just to demonstrate how each of them can also be an independent *CLI app*.
But for your final application, only `main.py` would need it.
///
## Recap
That's it, you can just add **Typer** applications one inside another as much as you want and create complex *CLI programs* while writing simple code.
You can probably achieve a simpler *CLI program* design that's easier to use than the example here. But if your requirements are complex, **Typer** helps you build your *CLI app* easily.
/// tip
Auto completion helps a lot, specially with complex programs.
Check the docs about adding auto completion to your *CLI apps*.
///
================================================
FILE: docs/tutorial/subcommands/single-file.md
================================================
# SubCommands in a Single File
In some cases, it's possible that your application code needs to live on a single file.
You can still use the same ideas:
{* docs_src/subcommands/tutorial002_py310/main.py *}
There are several things to notice here...
## Apps at the top
First, you can create `typer.Typer()` objects and add them to another one at the top.
It doesn't have to be done after creating the subcommands:
{* docs_src/subcommands/tutorial002_py310/main.py hl[4,5,6,7] *}
You can add the commands (subcommands) to each `typer.Typer()` app later and it will still work.
## Function names
As you now have subcommands like `create` for `users` and for `items`, you can no longer call the functions with just the name, like `def create()`, because they would overwrite each other.
So we use longer names:
{* docs_src/subcommands/tutorial002_py310/main.py hl[11,16,21,26,31] *}
## Command name
We are naming the functions with longer names so that they don't overwrite each other.
But we still want the subcommands to be `create`, `delete`, etc.
To call them like:
```console
// We want this ✔️
$ python main.py items create
```
instead of:
```console
// We don't want this ⛔️
$ python main.py items items-create
```
So we pass the name we want to use for each subcommand as the function argument to the decorator:
{* docs_src/subcommands/tutorial002_py310/main.py hl[10,15,20,25,30] *}
## Check it
It still works the same:
```console
// Check the help
$ python main.py --help
Usage: main.py [OPTIONS] COMMAND [ARGS]...
Options:
--install-completion Install completion for the current shell.
--show-completion Show completion for the current shell, to copy it or
customize the installation.
--help Show this message and exit.
Commands:
items
users
```
Check the `items` command:
```console
// Check the help for items
$ python main.py items --help
// It shows its own commands (subcommands): create, delete, sell
Usage: main.py items [OPTIONS] COMMAND [ARGS]...
Options:
--help Show this message and exit.
Commands:
create
delete
sell
// Try it
$ python main.py items create Wand
Creating item: Wand
$ python main.py items sell Vase
Selling item: Vase
```
And the same for the `users` command:
```console
$ python main.py users --help
Usage: main.py users [OPTIONS] COMMAND [ARGS]...
Options:
--help Show this message and exit.
Commands:
create
delete
// Try it
$ python main.py users create Camila
Creating user: Camila
```
================================================
FILE: docs/tutorial/terminating.md
================================================
# Terminating
There are some cases where you might want to terminate a command at some point, and stop all subsequent execution.
It could be that your code determined that the program completed successfully, or it could be an operation aborted.
## `Exit` a CLI program
You can normally just let the code of your CLI program finish its execution, but in some scenarios, you might want to terminate at some point in the middle of it. And prevent any subsequent code to run.
This doesn't have to mean that there's an error, just that nothing else needs to be executed.
In that case, you can raise a `typer.Exit()` exception:
{* docs_src/terminating/tutorial001_py310.py hl[9] *}
There are several things to see in this example.
* The CLI program is the function `main()`, not the others. This is the one that takes a *CLI argument*.
* The function `maybe_create_user()` can terminate the program by raising `typer.Exit()`.
* If the program is terminated by `maybe_create_user()` then `send_new_user_notification()` will never execute inside of `main()`.
Check it:
```console
$ python main.py Camila
User created: Camila
Notification sent for new user: Camila
// Try with an existing user
$ python main.py rick
The user already exists
// Notice that the notification code was never run, the second message is not printed
```
/// tip
Even though you are raising an exception, it doesn't necessarily mean there's an error.
This is done with an exception because it works as an "error" and stops all execution.
But then **Typer** catches it and just terminates the program normally.
///
## Exit with an error
`typer.Exit()` takes an optional `code` parameter. By default, `code` is `0`, meaning there was no error.
You can pass a `code` with a number other than `0` to tell the terminal that there was an error in the execution of the program:
{* docs_src/terminating/tutorial002_py310.py hl[10] *}
Check it:
```console
$ python main.py Camila
New user created: Camila
// Print the result code of the last program executed
$ echo $?
0
// Now make it exit with an error
$ python main.py root
The root user is reserved
// Print the result code of the last program executed
$ echo $?
1
// 1 means there was an error, 0 means no errors.
```
/// tip
The error code might be used by other programs (for example a Bash script) that execute your CLI program.
///
## Abort
There's a special exception that you can use to "abort" a program.
It works more or less the same as `typer.Exit()` but will print `"Aborted!"` to the screen and can be useful in certain cases later to make it explicit that the execution was aborted:
{* docs_src/terminating/tutorial003_py310.py hl[10] *}
Check it:
```console
$ python main.py Camila
New user created: Camila
// Now make it exit with an error
$ python main.py root
The root user is reserved
Aborted!
```
================================================
FILE: docs/tutorial/testing.md
================================================
# Testing
Testing **Typer** applications is very easy with pytest.
Let's say you have an application `app/main.py` with:
{* docs_src/testing/app01_py310/main.py *}
So, you would use it like:
```console
$ python main.py Camila --city Berlin
Hello Camila
Let's have a coffee in Berlin
```
And the directory also has an empty `app/__init__.py` file.
So, the `app` is a "Python package".
## Test the app
### Import and create a `CliRunner`
Create another file/module `app/test_main.py`.
Import `CliRunner` and create a `runner` object.
This runner is what will "invoke" or "call" your command line application.
{* docs_src/testing/app01_py310/test_main.py hl[1,5] *}
/// tip
It's important that the name of the file starts with `test_`, that way pytest will be able to detect it and use it automatically.
///
### Call the app
Then create a function `test_app()`.
And inside of the function, use the `runner` to `invoke` the application.
The first parameter to `runner.invoke()` is a `Typer` app.
The second parameter is a `list` of `str`, with all the text you would pass in the command line, right as you would pass it:
{* docs_src/testing/app01_py310/test_main.py hl[8,9] *}
/// tip
The name of the function has to start with `test_`, that way pytest can detect it and use it automatically.
///
### Check the result
Then, inside of the test function, add `assert` statements to ensure that everything in the result of the call is as it should be.
{* docs_src/testing/app01_py310/test_main.py hl[10,11,12] *}
Here we are checking that the exit code is 0, as it is for programs that exit without errors.
Then we check that the text printed to "standard output" contains the text that our CLI program prints.
/// tip
You could also check the output sent to "standard error" (`stderr`) or "standard output" (`stdout`) independently by accessing `result.stdout` and `result.stderr` in your tests.
///
/// info
If you need a refresher about what is "standard output" and "standard error" check the section in [Printing and Colors: "Standard Output" and "Standard Error"](printing.md#standard-output-and-standard-error){.internal-link target=_blank}.
///
### Call `pytest`
Then you can call `pytest` in your directory and it will run your tests:
```console
$ pytest
================ test session starts ================
platform linux -- Python 3.10, pytest-5.3.5, py-1.8.1, pluggy-0.13.1
rootdir: /home/user/code/superawesome-cli/app
plugins: forked-1.1.3, xdist-1.31.0, cov-2.8.1
collected 1 item
---> 100%
test_main.py . [100%]
================= 1 passed in 0.03s =================
```
## Testing input
If you have a CLI with prompts, like:
{* docs_src/testing/app02_an_py310/main.py hl[9] *}
That you would use like:
```console
$ python main.py Camila
# Email: $ camila@example.com
Hello Camila, your email is: camila@example.com
```
You can test the input typed in the terminal using `input="camila@example.com\n"`.
This is because what you type in the terminal goes to "**standard input**" and is handled by the operating system as if it was a "virtual file".
/// info
If you need a refresher about what is "standard output", "standard error", and "standard input" check the section in [Printing and Colors: "Standard Output" and "Standard Error"](printing.md#standard-output-and-standard-error){.internal-link target=_blank}.
///
When you hit the ENTER key after typing the email, that is just a "new line character". And in Python that is represented with `"\n"`.
So, if you use `input="camila@example.com\n"` it means: "type `camila@example.com` in the terminal, then hit the ENTER key":
{* docs_src/testing/app02_py310/test_main.py hl[9] *}
## Test a function
If you have a script and you never created an explicit `typer.Typer` app, like:
{* docs_src/testing/app03_py310/main.py hl[9] *}
...you can still test it, by creating an app during testing:
{* docs_src/testing/app03_py310/test_main.py hl[6,7,13] *}
Of course, if you are testing that script, it's probably easier/cleaner to just create the explicit `typer.Typer` app in `main.py` instead of creating it just during the test.
But if you want to keep it that way, e.g. because it's a simple example in documentation, then you can use that trick.
### About the `app.command` decorator
Notice the `app.command()(main)`.
If it's not obvious what it's doing, continue reading...
You would normally write something like:
```Python
@app.command()
def main(name: str = "World"):
# Some code here
```
But `@app.command()` is just a decorator.
That's equivalent to:
```Python
def main(name: str = "World"):
# Some code here
decorator = app.command()
new_main = decorator(main)
main = new_main
```
`app.command()` returns a function (`decorator`) that takes another function as it's only parameter (`main`).
And by using the `@something` you normally tell Python to replace the thing below (the function `main`) with the return of the `decorator` function (`new_main`).
Now, in the specific case of **Typer**, the decorator doesn't change the original function. It registers it internally and returns it unmodified.
So, `new_main` is actually the same original `main`.
So, in the case of **Typer**, as it doesn't really modify the decorated function, that would be equivalent to:
```Python
def main(name: str = "World"):
# Some code here
decorator = app.command()
decorator(main)
```
But then we don't need to create the variable `decorator` to use it below, we can just use it directly:
```Python
def main(name: str = "World"):
# Some code here
app.command()(main)
```
...that's it. It's still probably simpler to just create the explicit `typer.Typer` in the `main.py` file 😅.
================================================
FILE: docs/tutorial/typer-app.md
================================================
# Typer App
## Explicit application
So far, you have seen how to create a single function and then pass that function to `typer.run()`.
For example:
{* docs_src/first_steps/tutorial002_py310.py hl[9] *}
But that is actually a shortcut. Under the hood, **Typer** converts that to a CLI application with `typer.Typer()` and executes it. All that inside of `typer.run()`.
There's also a more explicit way to achieve the same:
{* docs_src/typer_app/tutorial001_py310.py hl[3,6,12] *}
When you use `typer.run()`, **Typer** is doing more or less the same as above, it will:
* Create a new `typer.Typer()` "application".
* Create a new "`command`" with your function.
* Call the same "application" as if it was a function with "`app()`".
/// info | `@decorator` Info
That `@something` syntax in Python is called a "decorator".
You put it on top of a function. Like a pretty decorative hat (I guess that's where the term came from).
A "decorator" takes the function below and does something with it.
In our case, this decorator tells **Typer** that the function below is a "`command`".
You will learn more about commands later in the section [commands](./commands/index.md){.internal-link target=_blank}.
///
Both ways, with `typer.run()` and creating the explicit application, achieve almost the same.
/// tip
If your use case is solved with just `typer.run()`, that's fine, you don't have to create the explicit `app` and use `@app.command()`, etc.
You might want to do that later when your app needs extra features, but if it doesn't need them yet, that's fine.
///
If you run the second example, with the explicit `app`, it works exactly the same:
```console
// Without a CLI argument
$ python main.py
Usage: main.py [OPTIONS] NAME
Try "main.py --help" for help.
Error: Missing argument 'NAME'.
// With the NAME CLI argument
$ python main.py Camila
Hello Camila
// Asking for help
$ python main.py --help
Usage: main.py [OPTIONS] NAME
Options:
--install-completion Install completion for the current shell.
--show-completion Show completion for the current shell, to copy it or customize the installation.
--help Show this message and exit.
```
## CLI application completion
There's a little detail that is worth noting here.
Now the help shows two new *CLI options*:
* `--install-completion`
* `--show-completion`
To get shell/tab completion, it's necessary to build a package that you and your users can install and **call directly**.
So instead of running a Python script like:
```console
$ python main.py
✨ Some magic here ✨
```
...It would be called like:
```console
$ magic-app
✨ Some magic here ✨
```
Having a standalone program like that allows setting up shell/tab completion.
The first step to be able to create an installable package like that is to use an explicit `typer.Typer()` app.
Later you can learn all the process to create a standalone CLI application and [Build a Package](./package.md){.internal-link target=_blank}.
But for now, it's just good to know that you are on that path. 😎
================================================
FILE: docs/tutorial/typer-command.md
================================================
# `typer` command
The `typer` command provides ✨ completion ✨ in the Terminal for your own small scripts. Even if they don't use Typer internally. Of course, it works better if you use **Typer** in your script.
It's probably most useful if you have a small custom Python script using **Typer** (maybe as part of some project), for some small tasks, and it's not complex/important enough to create a whole installable Python package for it (something to be installed with `pip`).
In that case, you can run your program with the `typer` command in your Terminal, and it will provide completion for your script.
The `typer` command also has functionality to generate Markdown documentation for your own **Typer** programs 📝.
## Install
When you install **Typer** with:
```bash
pip install typer
```
...it includes the `typer` command.
If you don't want to use the `typer` command, you can call the Typer library as a module with:
```bash
python -m typer
```
## Install completion
You can then install completion for the `typer` command with:
```console
$ typer --install-completion
bash completion installed in /home/user/.bashrc.
Completion will take effect once you restart the terminal.
```
### Sample script
Let's say you have a script that uses **Typer** in `my_custom_script.py`:
```Python
from typing import Optional
import typer
app = typer.Typer()
@app.command()
def hello(name: Optional[str] = None):
if name:
typer.echo(f"Hello {name}")
else:
typer.echo("Hello World!")
@app.command()
def bye(name: Optional[str] = None):
if name:
typer.echo(f"Bye {name}")
else:
typer.echo("Goodbye!")
if __name__ == "__main__":
app()
```
For it to work, you would also install **Typer**:
```console
$ python -m pip install typer
---> 100%
Successfully installed typer
```
### Run with Python
Then you could run your script with normal Python:
```console
$ python my_custom_script.py hello
Hello World!
$ python my_custom_script.py hello --name Camila
Hello Camila!
$ python my_custom_script.py bye --name Camila
Bye Camila
```
There's nothing wrong with using Python directly to run it. And, in fact, if some other code or program uses your script, that would probably be the best way to do it.
⛔️ But in your terminal, you won't get completion when hitting TAB for any of the subcommands or options, like `hello`, `bye`, and `--name`.
### Run with the `typer` command.
You can also run the same script with the `typer` command:
```console
$ typer my_custom_script.py run hello
Hello World!
$ typer my_custom_script.py run hello --name Camila
Hello Camila!
$ typer my_custom_script.py run bye --name Camila
Bye Camila
```
* Instead of using `python` directly you use the `typer` command.
* After the name of the file, add the subcommand `run`.
✔️ If you installed completion for the `typer` command as described above, when you hit TAB you will have ✨ completion for everything ✨, including all the subcommands and options of your script, like `hello`, `bye`, and `--name` 🚀.
## If main
Because the `typer` command won't use the block with:
```Python
if __name__ == "__main__":
app()
```
...you can also remove it if you are calling that script only with the `typer` command.
## Run other files
The `typer` command can run any script with **Typer**, but the script doesn't even have to use **Typer** at all.
You could even run a file with a function that could be used with `typer.run()`, even if the script doesn't use `typer.run()` or anything else.
For example, a file `main.py` like this will still work:
```Python
def main(name: str = "World"):
"""
Say hi to someone, by default to the World.
"""
print(f"Hello {name}")
```
Then you can call it with:
```console
$ typer main.py run --help
Usage: typer run [OPTIONS]
Say hi to someone, by default to the World.
Options:
--name TEXT
--help Show this message and exit.
$ typer main.py run --name Camila
Hello Camila
```
And it will also have completion for things like the `--name` *CLI Option*.
## Run a package or module
Instead of a file path you can pass a module (possibly in a package) to import.
For example:
```console
$ typer my_package.main run --help
Usage: typer run [OPTIONS]
Options:
--name TEXT
--help Show this message and exit.
$ typer my_package.main run --name Camila
Hello Camila
```
## Options
You can specify one of the following **CLI options**:
* `--app`: the name of the variable with a `Typer()` object to run as the main app.
* `--func`: the name of the variable with a function that would be used with `typer.run()`.
### Defaults
When your run a script with the `typer` command it will use the app from the following priority:
* An app object from the `--app` *CLI Option*.
* A function to convert to a **Typer** app from `--func` *CLI Option* (like when using `typer.run()`).
* A **Typer** app in a variable with a name of `app`, `cli`, or `main`.
* The first **Typer** app available in the file, with any name.
* A function in a variable with a name of `main`, `cli`, or `app`.
* The first function in the file, with any name.
## Generate docs
You can also use the `typer` command to generate Markdown documentation for your **Typer** application.
### Sample script with docs
For example, you could have a script like:
{* docs_src/commands/help/tutorial001_an_py310.py *}
### Generate docs with the `typer` command
Then you could generate docs for it with the `typer` command.
You can use the subcommand `utils`.
And then the subcommand `docs`.
```console
$ typer some_script.py utils docs
```
/// tip
If you don't want to use the `typer` command, you can still generate docs with:
```console
$ python -m typer some_script.py utils docs
```
///
**Options**:
* `--name TEXT`: The name of the CLI program to use in docs.
* `--output FILE`: An output file to write docs to, like README.md.
* `--title TEXT`: A title to use in the docs, by default the name of the command.
For example:
```console
$ typer my_package.main utils docs --name awesome-cli --output README.md
Docs saved to: README.md
```
### Sample docs output
For example, for the previous script, the generated docs would look like:
---
## `awesome-cli`
Awesome CLI user manager.
**Usage**:
```console
$ awesome-cli [OPTIONS] COMMAND [ARGS]...
```
**Options**:
* `--install-completion`: Install completion for the current shell.
* `--show-completion`: Show completion for the current shell, to copy it or customize the installation.
* `--help`: Show this message and exit.
**Commands**:
* `create`: Create a new user with USERNAME.
* `delete`: Delete a user with USERNAME.
* `delete-all`: Delete ALL users in the database.
* `init`: Initialize the users database.
## `awesome-cli create`
Create a new user with USERNAME.
**Usage**:
```console
$ awesome-cli create [OPTIONS] USERNAME
```
**Options**:
* `--help`: Show this message and exit.
## `awesome-cli delete`
Delete a user with USERNAME.
If --force is not used, will ask for confirmation.
**Usage**:
```console
$ awesome-cli delete [OPTIONS] USERNAME
```
**Options**:
* `--force / --no-force`: Force deletion without confirmation. [required]
* `--help`: Show this message and exit.
## `awesome-cli delete-all`
Delete ALL users in the database.
If --force is not used, will ask for confirmation.
**Usage**:
```console
$ awesome-cli delete-all [OPTIONS]
```
**Options**:
* `--force / --no-force`: Force deletion without confirmation. [required]
* `--help`: Show this message and exit.
## `awesome-cli init`
Initialize the users database.
**Usage**:
```console
$ awesome-cli init [OPTIONS]
```
**Options**:
* `--help`: Show this message and exit.
================================================
FILE: docs/virtual-environments.md
================================================
# Virtual Environments
When you work in Python projects you probably should use a **virtual environment** (or a similar mechanism) to isolate the packages you install for each project.
/// info
If you already know about virtual environments, how to create them and use them, you might want to skip this section. 🤓
///
/// tip
A **virtual environment** is different than an **environment variable**.
An **environment variable** is a variable in the system that can be used by programs.
A **virtual environment** is a directory with some files in it.
///
/// info
This page will teach you how to use **virtual environments** and how they work.
If you are ready to adopt a **tool that manages everything** for you (including installing Python), try uv.
///
## Create a Project
First, create a directory for your project.
What I normally do is that I create a directory named `code` inside my home/user directory.
And inside of that I create one directory per project.
```console
// Go to the home directory
$ cd
// Create a directory for all your code projects
$ mkdir code
// Enter into that code directory
$ cd code
// Create a directory for this project
$ mkdir awesome-project
// Enter into that project directory
$ cd awesome-project
```
## Create a Virtual Environment
When you start working on a Python project **for the first time**, create a virtual environment **inside your project**.
/// tip
You only need to do this **once per project**, not every time you work.
///
//// tab | `venv`
To create a virtual environment, you can use the `venv` module that comes with Python.
```console
$ python -m venv .venv
```
/// details | What that command means
* `python`: use the program called `python`
* `-m`: call a module as a script, we'll tell it which module next
* `venv`: use the module called `venv` that normally comes installed with Python
* `.venv`: create the virtual environment in the new directory `.venv`
///
////
//// tab | `uv`
If you have `uv` installed, you can use it to create a virtual environment.
```console
$ uv venv
```
/// tip
By default, `uv` will create a virtual environment in a directory called `.venv`.
But you could customize it passing an additional argument with the directory name.
///
////
That command creates a new virtual environment in a directory called `.venv`.
/// details | `.venv` or other name
You could create the virtual environment in a different directory, but there's a convention of calling it `.venv`.
///
## Activate the Virtual Environment
Activate the new virtual environment so that any Python command you run or package you install uses it.
/// tip
Do this **every time** you start a **new terminal session** to work on the project.
///
//// tab | Linux, macOS
```console
$ source .venv/bin/activate
```
////
//// tab | Windows PowerShell
```console
$ .venv\Scripts\Activate.ps1
```
////
//// tab | Windows Bash
Or if you use Bash for Windows (e.g. Git Bash):
```console
$ source .venv/Scripts/activate
```
////
/// tip
Every time you install a **new package** in that environment, **activate** the environment again.
This makes sure that if you use a **terminal (CLI) program** installed by that package, you use the one from your virtual environment and not any other that could be installed globally, probably with a different version than what you need.
///
## Check the Virtual Environment is Active
Check that the virtual environment is active (the previous command worked).
/// tip
This is **optional**, but it's a good way to **check** that everything is working as expected and you are using the virtual environment you intended.
///
//// tab | Linux, macOS, Windows Bash
```console
$ which python
/home/user/code/awesome-project/.venv/bin/python
```
If it shows the `python` binary at `.venv/bin/python`, inside of your project (in this case `awesome-project`), then it worked. 🎉
////
//// tab | Windows PowerShell
```console
$ Get-Command python
C:\Users\user\code\awesome-project\.venv\Scripts\python
```
If it shows the `python` binary at `.venv\Scripts\python`, inside of your project (in this case `awesome-project`), then it worked. 🎉
////
## Upgrade `pip`
/// tip
If you use `uv` you would use it to install things instead of `pip`, so you don't need to upgrade `pip`. 😎
///
If you are using `pip` to install packages (it comes by default with Python), you should **upgrade** it to the latest version.
Many exotic errors while installing a package are solved by just upgrading `pip` first.
/// tip
You would normally do this **once**, right after you create the virtual environment.
///
Make sure the virtual environment is active (with the command above) and then run:
```console
$ python -m pip install --upgrade pip
---> 100%
```
## Add `.gitignore`
If you are using **Git** (you should), add a `.gitignore` file to exclude everything in your `.venv` from Git.
/// tip
If you used `uv` to create the virtual environment, it already did this for you, you can skip this step. 😎
///
/// tip
Do this **once**, right after you create the virtual environment.
///
```console
$ echo "*" > .venv/.gitignore
```
/// details | What that command means
* `echo "*"`: will "print" the text `*` in the terminal (the next part changes that a bit)
* `>`: anything printed to the terminal by the command to the left of `>` should not be printed but instead written to the file that goes to the right of `>`
* `.gitignore`: the name of the file where the text should be written
And `*` for Git means "everything". So, it will ignore everything in the `.venv` directory.
That command will create a file `.gitignore` with the content:
```gitignore
*
```
///
## Install Packages
After activating the environment, you can install packages in it.
/// tip
Do this **once** when installing or upgrading the packages your project needs.
If you need to upgrade a version or add a new package you would **do this again**.
///
### Install Packages Directly
If you're in a hurry and don't want to use a file to declare your project's package requirements, you can install them directly.
/// tip
It's a (very) good idea to put the packages and versions your program needs in a file (for example `requirements.txt` or `pyproject.toml`).
///
//// tab | `pip`
```console
$ pip install typer
---> 100%
```
////
//// tab | `uv`
If you have `uv`:
```console
$ uv pip install typer
---> 100%
```
////
### Install from `requirements.txt`
If you have a `requirements.txt`, you can now use it to install its packages.
//// tab | `pip`
```console
$ pip install -r requirements.txt
---> 100%
```
////
//// tab | `uv`
If you have `uv`:
```console
$ uv pip install -r requirements.txt
---> 100%
```
////
/// details | `requirements.txt`
A `requirements.txt` with some packages could look like:
```requirements.txt
typer==0.13.0
rich==13.7.1
```
///
## Run Your Program
After you activated the virtual environment, you can run your program, and it will use the Python inside of your virtual environment with the packages you installed there.
```console
$ python main.py
Hello World
```
## Configure Your Editor
You would probably use an editor, make sure you configure it to use the same virtual environment you created (it will probably autodetect it) so that you can get autocompletion and inline errors.
For example:
* VS Code
* PyCharm
/// tip
You normally have to do this only **once**, when you create the virtual environment.
///
## Deactivate the Virtual Environment
Once you are done working on your project you can **deactivate** the virtual environment.
```console
$ deactivate
```
This way, when you run `python` it won't try to run it from that virtual environment with the packages installed there.
## Ready to Work
Now you're ready to start working on your project.
/// tip
Do you want to understand what's all that above?
Continue reading. 👇🤓
///
## Why Virtual Environments
To work with Typer you need to install Python.
After that, you would need to **install** Typer and any other **packages** you want to use.
To install packages you would normally use the `pip` command that comes with Python (or similar alternatives).
Nevertheless, if you just use `pip` directly, the packages would be installed in your **global Python environment** (the global installation of Python).
### The Problem
So, what's the problem with installing packages in the global Python environment?
At some point, you will probably end up writing many different programs that depend on **different packages**. And some of these projects you work on will depend on **different versions** of the same package. 😱
For example, you could create a project called `philosophers-stone`, this program depends on another package called **`harry`, using the version `1`**. So, you need to install `harry`.
```mermaid
flowchart LR
stone(philosophers-stone) -->|requires| harry-1[harry v1]
```
Then, at some point later, you create another project called `prisoner-of-azkaban`, and this project also depends on `harry`, but this project needs **`harry` version `3`**.
```mermaid
flowchart LR
azkaban(prisoner-of-azkaban) --> |requires| harry-3[harry v3]
```
But now the problem is, if you install the packages globally (in the global environment) instead of in a local **virtual environment**, you will have to choose which version of `harry` to install.
If you want to run `philosophers-stone` you will need to first install `harry` version `1`, for example with:
```console
$ pip install "harry==1"
```
And then you would end up with `harry` version `1` installed in your global Python environment.
```mermaid
flowchart LR
subgraph global[global env]
harry-1[harry v1]
end
subgraph stone-project[philosophers-stone project]
stone(philosophers-stone) -->|requires| harry-1
end
```
But then if you want to run `prisoner-of-azkaban`, you will need to uninstall `harry` version `1` and install `harry` version `3` (or just installing version `3` would automatically uninstall version `1`).
```console
$ pip install "harry==3"
```
And then you would end up with `harry` version `3` installed in your global Python environment.
And if you try to run `philosophers-stone` again, there's a chance it would **not work** because it needs `harry` version `1`.
```mermaid
flowchart LR
subgraph global[global env]
harry-1[harry v1]
style harry-1 fill:#ccc,stroke-dasharray: 5 5
harry-3[harry v3]
end
subgraph stone-project[philosophers-stone project]
stone(philosophers-stone) -.-x|⛔️| harry-1
end
subgraph azkaban-project[prisoner-of-azkaban project]
azkaban(prisoner-of-azkaban) --> |requires| harry-3
end
```
/// tip
It's very common in Python packages to try the best to **avoid breaking changes** in **new versions**, but it's better to be safe, and install newer versions intentionally and when you can run the tests to check everything is working correctly.
///
Now, imagine that with **many** other **packages** that all your **projects depend on**. That's very difficult to manage. And you would probably end up running some projects with some **incompatible versions** of the packages, and not knowing why something isn't working.
Also, depending on your operating system (e.g. Linux, Windows, macOS), it could have come with Python already installed. And in that case it probably had some packages pre-installed with some specific versions **needed by your system**. If you install packages in the global Python environment, you could end up **breaking** some of the programs that came with your operating system.
## Where are Packages Installed
When you install Python, it creates some directories with some files in your computer.
Some of these directories are the ones in charge of having all the packages you install.
When you run:
```console
// Don't run this now, it's just an example 🤓
$ pip install typer
---> 100%
```
That will download a compressed file with the Typer code, normally from PyPI.
It will also **download** files for other packages that Typer depends on.
Then it will **extract** all those files and put them in a directory in your computer.
By default, it will put those files downloaded and extracted in the directory that comes with your Python installation, that's the **global environment**.
## What are Virtual Environments
The solution to the problems of having all the packages in the global environment is to use a **virtual environment for each project** you work on.
A virtual environment is a **directory**, very similar to the global one, where you can install the packages for a project.
This way, each project will have its own virtual environment (`.venv` directory) with its own packages.
```mermaid
flowchart TB
subgraph stone-project[philosophers-stone project]
stone(philosophers-stone) --->|requires| harry-1
subgraph venv1[.venv]
harry-1[harry v1]
end
end
subgraph azkaban-project[prisoner-of-azkaban project]
azkaban(prisoner-of-azkaban) --->|requires| harry-3
subgraph venv2[.venv]
harry-3[harry v3]
end
end
stone-project ~~~ azkaban-project
```
## What Does Activating a Virtual Environment Mean
When you activate a virtual environment, for example with:
//// tab | Linux, macOS
```console
$ source .venv/bin/activate
```
////
//// tab | Windows PowerShell
```console
$ .venv\Scripts\Activate.ps1
```
////
//// tab | Windows Bash
Or if you use Bash for Windows (e.g. Git Bash):
```console
$ source .venv/Scripts/activate
```
////
That command will create or modify some [environment variables](environment-variables.md){.internal-link target=_blank} that will be available for the next commands.
One of those variables is the `PATH` variable.
/// tip
You can learn more about the `PATH` environment variable in the [Environment Variables](environment-variables.md#path-environment-variable){.internal-link target=_blank} section.
///
Activating a virtual environment adds its path `.venv/bin` (on Linux and macOS) or `.venv\Scripts` (on Windows) to the `PATH` environment variable.
Let's say that before activating the environment, the `PATH` variable looked like this:
//// tab | Linux, macOS
```plaintext
/usr/bin:/bin:/usr/sbin:/sbin
```
That means that the system would look for programs in:
* `/usr/bin`
* `/bin`
* `/usr/sbin`
* `/sbin`
////
//// tab | Windows
```plaintext
C:\Windows\System32
```
That means that the system would look for programs in:
* `C:\Windows\System32`
////
After activating the virtual environment, the `PATH` variable would look something like this:
//// tab | Linux, macOS
```plaintext
/home/user/code/awesome-project/.venv/bin:/usr/bin:/bin:/usr/sbin:/sbin
```
That means that the system will now start looking first look for programs in:
```plaintext
/home/user/code/awesome-project/.venv/bin
```
before looking in the other directories.
So, when you type `python` in the terminal, the system will find the Python program in
```plaintext
/home/user/code/awesome-project/.venv/bin/python
```
and use that one.
////
//// tab | Windows
```plaintext
C:\Users\user\code\awesome-project\.venv\Scripts;C:\Windows\System32
```
That means that the system will now start looking first look for programs in:
```plaintext
C:\Users\user\code\awesome-project\.venv\Scripts
```
before looking in the other directories.
So, when you type `python` in the terminal, the system will find the Python program in
```plaintext
C:\Users\user\code\awesome-project\.venv\Scripts\python
```
and use that one.
////
An important detail is that it will put the virtual environment path at the **beginning** of the `PATH` variable. The system will find it **before** finding any other Python available. This way, when you run `python`, it will use the Python **from the virtual environment** instead of any other `python` (for example, a `python` from a global environment).
Activating a virtual environment also changes a couple of other things, but this is one of the most important things it does.
## Checking a Virtual Environment
When you check if a virtual environment is active, for example with:
//// tab | Linux, macOS, Windows Bash
```console
$ which python
/home/user/code/awesome-project/.venv/bin/python
```
////
//// tab | Windows PowerShell
```console
$ Get-Command python
C:\Users\user\code\awesome-project\.venv\Scripts\python
```
////
That means that the `python` program that will be used is the one **in the virtual environment**.
you use `which` in Linux and macOS and `Get-Command` in Windows PowerShell.
The way that command works is that it will go and check in the `PATH` environment variable, going through **each path in order**, looking for the program called `python`. Once it finds it, it will **show you the path** to that program.
The most important part is that when you call `python`, that is the exact "`python`" that will be executed.
So, you can confirm if you are in the correct virtual environment.
/// tip
It's easy to activate one virtual environment, get one Python, and then **go to another project**.
And the second project **wouldn't work** because you are using the **incorrect Python**, from a virtual environment for another project.
It's useful being able to check what `python` is being used. 🤓
///
## Why Deactivate a Virtual Environment
For example, you could be working on a project `philosophers-stone`, **activate that virtual environment**, install packages and work with that environment.
And then you want to work on **another project** `prisoner-of-azkaban`.
You go to that project:
```console
$ cd ~/code/prisoner-of-azkaban
```
If you don't deactivate the virtual environment for `philosophers-stone`, when you run `python` in the terminal, it will try to use the Python from `philosophers-stone`.
```console
$ cd ~/code/prisoner-of-azkaban
$ python main.py
// Error importing sirius, it's not installed 😱
Traceback (most recent call last):
File "main.py", line 1, in
import sirius
```
But if you deactivate the virtual environment and activate the new one for `prisoner-of-askaban` then when you run `python` it will use the Python from the virtual environment in `prisoner-of-azkaban`.
```console
$ cd ~/code/prisoner-of-azkaban
// You don't need to be in the old directory to deactivate, you can do it wherever you are, even after going to the other project 😎
$ deactivate
// Activate the virtual environment in prisoner-of-azkaban/.venv 🚀
$ source .venv/bin/activate
// Now when you run python, it will find the package sirius installed in this virtual environment ✨
$ python main.py
I solemnly swear 🐺
```
## Alternatives
This is a simple guide to get you started and teach you how everything works **underneath**.
There are many **alternatives** to managing virtual environments, package dependencies (requirements), projects.
Once you are ready and want to use a tool to **manage the entire project**, packages dependencies, virtual environments, etc. I would suggest you try uv.
`uv` can do a lot of things, it can:
* **Install Python** for you, including different versions
* Manage the **virtual environment** for your projects
* Install **packages**
* Manage package **dependencies and versions** for your project
* Make sure you have an **exact** set of packages and versions to install, including their dependencies, so that you can be sure that you can run your project in production exactly the same as in your computer while developing, this is called **locking**
* And many other things
## Conclusion
If you read and understood all this, now **you know much more** about virtual environments than many developers out there. 🤓
Knowing these details will most probably be useful in a future time when you are debugging something that seems complex, but you will know **how it all works underneath**. 😎
================================================
FILE: docs_src/app_dir/__init__.py
================================================
================================================
FILE: docs_src/app_dir/tutorial001_py310.py
================================================
from pathlib import Path
import typer
APP_NAME = "my-super-cli-app"
app = typer.Typer()
@app.command()
def main():
app_dir = typer.get_app_dir(APP_NAME)
config_path: Path = Path(app_dir) / "config.json"
if not config_path.is_file():
print("Config file doesn't exist yet")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/arguments/__init__.py
================================================
================================================
FILE: docs_src/arguments/default/__init__.py
================================================
================================================
FILE: docs_src/arguments/default/tutorial001_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(name: Annotated[str, typer.Argument()] = "Wade Wilson"):
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/arguments/default/tutorial001_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(name: str = typer.Argument("Wade Wilson")):
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/arguments/default/tutorial002_an_py310.py
================================================
import random
from typing import Annotated
import typer
app = typer.Typer()
def get_name():
return random.choice(["Deadpool", "Rick", "Morty", "Hiro"])
@app.command()
def main(name: Annotated[str, typer.Argument(default_factory=get_name)]):
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/arguments/default/tutorial002_py310.py
================================================
import random
import typer
app = typer.Typer()
def get_name():
return random.choice(["Deadpool", "Rick", "Morty", "Hiro"])
@app.command()
def main(name: str = typer.Argument(default_factory=get_name)):
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/arguments/envvar/__init__.py
================================================
================================================
FILE: docs_src/arguments/envvar/tutorial001_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(name: Annotated[str, typer.Argument(envvar="AWESOME_NAME")] = "World"):
print(f"Hello Mr. {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/arguments/envvar/tutorial001_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(name: str = typer.Argument("World", envvar="AWESOME_NAME")):
print(f"Hello Mr. {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/arguments/envvar/tutorial002_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(
name: Annotated[str, typer.Argument(envvar=["AWESOME_NAME", "GOD_NAME"])] = "World",
):
print(f"Hello Mr. {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/arguments/envvar/tutorial002_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(name: str = typer.Argument("World", envvar=["AWESOME_NAME", "GOD_NAME"])):
print(f"Hello Mr. {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/arguments/envvar/tutorial003_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(
name: Annotated[
str, typer.Argument(envvar="AWESOME_NAME", show_envvar=False)
] = "World",
):
print(f"Hello Mr. {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/arguments/envvar/tutorial003_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(name: str = typer.Argument("World", envvar="AWESOME_NAME", show_envvar=False)):
print(f"Hello Mr. {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/arguments/help/__init__.py
================================================
================================================
FILE: docs_src/arguments/help/tutorial001_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(name: Annotated[str, typer.Argument(help="The name of the user to greet")]):
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/arguments/help/tutorial001_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(name: str = typer.Argument(..., help="The name of the user to greet")):
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/arguments/help/tutorial002_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(name: Annotated[str, typer.Argument(help="The name of the user to greet")]):
"""
Say hi to NAME very gently, like Dirk.
"""
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/arguments/help/tutorial002_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(name: str = typer.Argument(..., help="The name of the user to greet")):
"""
Say hi to NAME very gently, like Dirk.
"""
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/arguments/help/tutorial003_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(name: Annotated[str, typer.Argument(help="Who to greet")] = "World"):
"""
Say hi to NAME very gently, like Dirk.
"""
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/arguments/help/tutorial003_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(name: str = typer.Argument("World", help="Who to greet")):
"""
Say hi to NAME very gently, like Dirk.
"""
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/arguments/help/tutorial004_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(
name: Annotated[
str, typer.Argument(help="Who to greet", show_default=False)
] = "World",
):
"""
Say hi to NAME very gently, like Dirk.
"""
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/arguments/help/tutorial004_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(name: str = typer.Argument("World", help="Who to greet", show_default=False)):
"""
Say hi to NAME very gently, like Dirk.
"""
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/arguments/help/tutorial005_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(
name: Annotated[
str,
typer.Argument(
help="Who to greet", show_default="Deadpoolio the amazing's name"
),
] = "Wade Wilson",
):
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/arguments/help/tutorial005_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(
name: str = typer.Argument(
"Wade Wilson", help="Who to greet", show_default="Deadpoolio the amazing's name"
),
):
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/arguments/help/tutorial006_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(name: Annotated[str, typer.Argument(metavar="✨username✨")] = "World"):
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/arguments/help/tutorial006_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(name: str = typer.Argument("World", metavar="✨username✨")):
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/arguments/help/tutorial007_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(
name: Annotated[str, typer.Argument(help="Who to greet")],
lastname: Annotated[
str, typer.Argument(help="The last name", rich_help_panel="Secondary Arguments")
] = "",
age: Annotated[
str,
typer.Argument(help="The user's age", rich_help_panel="Secondary Arguments"),
] = "",
):
"""
Say hi to NAME very gently, like Dirk.
"""
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/arguments/help/tutorial007_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(
name: str = typer.Argument(..., help="Who to greet"),
lastname: str = typer.Argument(
"", help="The last name", rich_help_panel="Secondary Arguments"
),
age: str = typer.Argument(
"", help="The user's age", rich_help_panel="Secondary Arguments"
),
):
"""
Say hi to NAME very gently, like Dirk.
"""
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/arguments/help/tutorial008_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(name: Annotated[str, typer.Argument(hidden=True)] = "World"):
"""
Say hi to NAME very gently, like Dirk.
"""
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/arguments/help/tutorial008_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(name: str = typer.Argument("World", hidden=True)):
"""
Say hi to NAME very gently, like Dirk.
"""
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/arguments/optional/__init__.py
================================================
================================================
FILE: docs_src/arguments/optional/tutorial000_an_py310.py
================================================
from typing import Annotated
import typer
def main(name: Annotated[str, typer.Argument()]):
print(f"Hello {name}")
if __name__ == "__main__":
typer.run(main)
================================================
FILE: docs_src/arguments/optional/tutorial000_py310.py
================================================
import typer
def main(name: str = typer.Argument()):
print(f"Hello {name}")
if __name__ == "__main__":
typer.run(main)
================================================
FILE: docs_src/arguments/optional/tutorial001_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(name: Annotated[str, typer.Argument()]):
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/arguments/optional/tutorial001_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(name: str = typer.Argument()):
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/arguments/optional/tutorial002_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(name: Annotated[str, typer.Argument()] = "World"):
print(f"Hello {name}!")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/arguments/optional/tutorial002_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(name: str = typer.Argument(default="World")):
print(f"Hello {name}!")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/arguments/optional/tutorial003_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(name: str = typer.Argument(default=...)):
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/commands/__init__.py
================================================
================================================
FILE: docs_src/commands/arguments/__init__.py
================================================
================================================
FILE: docs_src/commands/arguments/tutorial001_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def create(username: str):
print(f"Creating user: {username}")
@app.command()
def delete(username: str):
print(f"Deleting user: {username}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/commands/callback/__init__.py
================================================
================================================
FILE: docs_src/commands/callback/tutorial001_py310.py
================================================
import typer
app = typer.Typer()
state = {"verbose": False}
@app.command()
def create(username: str):
if state["verbose"]:
print("About to create a user")
print(f"Creating user: {username}")
if state["verbose"]:
print("Just created a user")
@app.command()
def delete(username: str):
if state["verbose"]:
print("About to delete a user")
print(f"Deleting user: {username}")
if state["verbose"]:
print("Just deleted a user")
@app.callback()
def main(verbose: bool = False):
"""
Manage users in the awesome CLI app.
"""
if verbose:
print("Will write verbose output")
state["verbose"] = True
if __name__ == "__main__":
app()
================================================
FILE: docs_src/commands/callback/tutorial002_py310.py
================================================
import typer
def callback():
print("Running a command")
app = typer.Typer(callback=callback)
@app.command()
def create(name: str):
print(f"Creating user: {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/commands/callback/tutorial003_py310.py
================================================
import typer
def callback():
print("Running a command")
app = typer.Typer(callback=callback)
@app.callback()
def new_callback():
print("Override callback, running a command")
@app.command()
def create(name: str):
print(f"Creating user: {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/commands/callback/tutorial004_py310.py
================================================
import typer
app = typer.Typer()
@app.callback()
def callback():
"""
Manage users CLI app.
Use it with the create command.
A new user with the given NAME will be created.
"""
@app.command()
def create(name: str):
print(f"Creating user: {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/commands/context/__init__.py
================================================
================================================
FILE: docs_src/commands/context/tutorial001_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def create(username: str):
print(f"Creating user: {username}")
@app.command()
def delete(username: str):
print(f"Deleting user: {username}")
@app.callback()
def main(ctx: typer.Context):
"""
Manage users in the awesome CLI app.
"""
print(f"About to execute command: {ctx.invoked_subcommand}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/commands/context/tutorial002_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def create(username: str):
print(f"Creating user: {username}")
@app.command()
def delete(username: str):
print(f"Deleting user: {username}")
@app.callback(invoke_without_command=True)
def main():
"""
Manage users in the awesome CLI app.
"""
print("Initializing database")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/commands/context/tutorial003_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def create(username: str):
print(f"Creating user: {username}")
@app.command()
def delete(username: str):
print(f"Deleting user: {username}")
@app.callback(invoke_without_command=True)
def main(ctx: typer.Context):
"""
Manage users in the awesome CLI app.
"""
if ctx.invoked_subcommand is None:
print("Initializing database")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/commands/context/tutorial004_py310.py
================================================
import typer
app = typer.Typer()
@app.command(
context_settings={"allow_extra_args": True, "ignore_unknown_options": True}
)
def main(ctx: typer.Context):
for extra_arg in ctx.args:
print(f"Got extra arg: {extra_arg}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/commands/help/__init__.py
================================================
================================================
FILE: docs_src/commands/help/tutorial001_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer(help="Awesome CLI user manager.")
@app.command()
def create(username: str):
"""
Create a new user with USERNAME.
"""
print(f"Creating user: {username}")
@app.command()
def delete(
username: str,
force: Annotated[
bool,
typer.Option(
prompt="Are you sure you want to delete the user?",
help="Force deletion without confirmation.",
),
],
):
"""
Delete a user with USERNAME.
If --force is not used, will ask for confirmation.
"""
if force:
print(f"Deleting user: {username}")
else:
print("Operation cancelled")
@app.command()
def delete_all(
force: Annotated[
bool,
typer.Option(
prompt="Are you sure you want to delete ALL users?",
help="Force deletion without confirmation.",
),
],
):
"""
Delete ALL users in the database.
If --force is not used, will ask for confirmation.
"""
if force:
print("Deleting all users")
else:
print("Operation cancelled")
@app.command()
def init():
"""
Initialize the users database.
"""
print("Initializing user database")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/commands/help/tutorial001_py310.py
================================================
import typer
app = typer.Typer(help="Awesome CLI user manager.")
@app.command()
def create(username: str):
"""
Create a new user with USERNAME.
"""
print(f"Creating user: {username}")
@app.command()
def delete(
username: str,
force: bool = typer.Option(
...,
prompt="Are you sure you want to delete the user?",
help="Force deletion without confirmation.",
),
):
"""
Delete a user with USERNAME.
If --force is not used, will ask for confirmation.
"""
if force:
print(f"Deleting user: {username}")
else:
print("Operation cancelled")
@app.command()
def delete_all(
force: bool = typer.Option(
...,
prompt="Are you sure you want to delete ALL users?",
help="Force deletion without confirmation.",
),
):
"""
Delete ALL users in the database.
If --force is not used, will ask for confirmation.
"""
if force:
print("Deleting all users")
else:
print("Operation cancelled")
@app.command()
def init():
"""
Initialize the users database.
"""
print("Initializing user database")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/commands/help/tutorial002_py310.py
================================================
import typer
app = typer.Typer()
@app.command(help="Create a new user with USERNAME.")
def create(username: str):
"""
Some internal utility function to create.
"""
print(f"Creating user: {username}")
@app.command(help="Delete a user with USERNAME.")
def delete(username: str):
"""
Some internal utility function to delete.
"""
print(f"Deleting user: {username}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/commands/help/tutorial003_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def create(username: str):
"""
Create a user.
"""
print(f"Creating user: {username}")
@app.command(deprecated=True)
def delete(username: str):
"""
Delete a user.
This is deprecated and will stop being supported soon.
"""
print(f"Deleting user: {username}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/commands/help/tutorial004_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer(rich_markup_mode="rich")
@app.command()
def create(
username: Annotated[
str, typer.Argument(help="The username to be [green]created[/green]")
],
):
"""
[bold green]Create[/bold green] a new [italic]shiny[/italic] user. :sparkles:
This requires a [underline]username[/underline].
"""
print(f"Creating user: {username}")
@app.command(help="[bold red]Delete[/bold red] a user with [italic]USERNAME[/italic].")
def delete(
username: Annotated[
str, typer.Argument(help="The username to be [red]deleted[/red]")
],
force: Annotated[
bool, typer.Option(help="Force the [bold red]deletion[/bold red] :boom:")
] = False,
):
"""
Some internal utility function to delete.
"""
print(f"Deleting user: {username}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/commands/help/tutorial004_py310.py
================================================
import typer
app = typer.Typer(rich_markup_mode="rich")
@app.command()
def create(
username: str = typer.Argument(
..., help="The username to be [green]created[/green]"
),
):
"""
[bold green]Create[/bold green] a new [italic]shiny[/italic] user. :sparkles:
This requires a [underline]username[/underline].
"""
print(f"Creating user: {username}")
@app.command(help="[bold red]Delete[/bold red] a user with [italic]USERNAME[/italic].")
def delete(
username: str = typer.Argument(..., help="The username to be [red]deleted[/red]"),
force: bool = typer.Option(
False, help="Force the [bold red]deletion[/bold red] :boom:"
),
):
"""
Some internal utility function to delete.
"""
print(f"Deleting user: {username}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/commands/help/tutorial005_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer(rich_markup_mode="markdown")
@app.command()
def create(
username: Annotated[str, typer.Argument(help="The username to be **created**")],
):
"""
**Create** a new *shiny* user. :sparkles:
* Create a username
* Show that the username is created
---
Learn more at the [Typer docs website](https://typer.tiangolo.com)
"""
print(f"Creating user: {username}")
@app.command(help="**Delete** a user with *USERNAME*.")
def delete(
username: Annotated[str, typer.Argument(help="The username to be **deleted**")],
force: Annotated[bool, typer.Option(help="Force the **deletion** :boom:")] = False,
):
"""
Some internal utility function to delete.
"""
print(f"Deleting user: {username}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/commands/help/tutorial005_py310.py
================================================
import typer
app = typer.Typer(rich_markup_mode="markdown")
@app.command()
def create(username: str = typer.Argument(..., help="The username to be **created**")):
"""
**Create** a new *shiny* user. :sparkles:
* Create a username
* Show that the username is created
---
Learn more at the [Typer docs website](https://typer.tiangolo.com)
"""
print(f"Creating user: {username}")
@app.command(help="**Delete** a user with *USERNAME*.")
def delete(
username: str = typer.Argument(..., help="The username to be **deleted**"),
force: bool = typer.Option(False, help="Force the **deletion** :boom:"),
):
"""
Some internal utility function to delete.
"""
print(f"Deleting user: {username}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/commands/help/tutorial006_py310.py
================================================
import typer
app = typer.Typer(rich_markup_mode="rich")
@app.command()
def create(username: str):
"""
[green]Create[/green] a new user. :sparkles:
"""
print(f"Creating user: {username}")
@app.command()
def delete(username: str):
"""
[red]Delete[/red] a user. :x:
"""
print(f"Deleting user: {username}")
@app.command(rich_help_panel="Utils and Configs")
def config(configuration: str):
"""
[blue]Configure[/blue] the system. :gear:
"""
print(f"Configuring the system with: {configuration}")
@app.command(rich_help_panel="Utils and Configs")
def sync():
"""
[blue]Synchronize[/blue] the system or something fancy like that. :recycle:
"""
print("Syncing the system")
@app.command(rich_help_panel="Help and Others")
def help():
"""
Get [yellow]help[/yellow] with the system. :question:
"""
print("Opening help portal...")
@app.command(rich_help_panel="Help and Others")
def report():
"""
[yellow]Report[/yellow] an issue. :exclamation:
"""
print("Please open a new issue online, not a direct message")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/commands/help/tutorial007_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer(rich_markup_mode="rich")
@app.command()
def create(
username: Annotated[str, typer.Argument(help="The username to create")],
lastname: Annotated[
str,
typer.Argument(
help="The last name of the new user", rich_help_panel="Secondary Arguments"
),
] = "",
force: Annotated[bool, typer.Option(help="Force the creation of the user")] = False,
age: Annotated[
int | None,
typer.Option(help="The age of the new user", rich_help_panel="Additional Data"),
] = None,
favorite_color: Annotated[
str | None,
typer.Option(
help="The favorite color of the new user",
rich_help_panel="Additional Data",
),
] = None,
):
"""
[green]Create[/green] a new user. :sparkles:
"""
print(f"Creating user: {username}")
@app.command(rich_help_panel="Utils and Configs")
def config(configuration: str):
"""
[blue]Configure[/blue] the system. :gear:
"""
print(f"Configuring the system with: {configuration}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/commands/help/tutorial007_py310.py
================================================
import typer
app = typer.Typer(rich_markup_mode="rich")
@app.command()
def create(
username: str = typer.Argument(..., help="The username to create"),
lastname: str = typer.Argument(
"", help="The last name of the new user", rich_help_panel="Secondary Arguments"
),
force: bool = typer.Option(False, help="Force the creation of the user"),
age: int | None = typer.Option(
None, help="The age of the new user", rich_help_panel="Additional Data"
),
favorite_color: str | None = typer.Option(
None,
help="The favorite color of the new user",
rich_help_panel="Additional Data",
),
):
"""
[green]Create[/green] a new user. :sparkles:
"""
print(f"Creating user: {username}")
@app.command(rich_help_panel="Utils and Configs")
def config(configuration: str):
"""
[blue]Configure[/blue] the system. :gear:
"""
print(f"Configuring the system with: {configuration}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/commands/help/tutorial008_py310.py
================================================
import typer
app = typer.Typer(rich_markup_mode="rich")
@app.command(epilog="Made with :heart: in [blue]Venus[/blue]")
def create(username: str):
"""
[green]Create[/green] a new user. :sparkles:
"""
print(f"Creating user: {username}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/commands/index/__init__.py
================================================
================================================
FILE: docs_src/commands/index/tutorial002_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def create():
print("Creating user: Hiro Hamada")
@app.command()
def delete():
print("Deleting user: Hiro Hamada")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/commands/index/tutorial003_py310.py
================================================
import typer
app = typer.Typer(no_args_is_help=True)
@app.command()
def create():
print("Creating user: Hiro Hamada")
@app.command()
def delete():
print("Deleting user: Hiro Hamada")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/commands/index/tutorial004_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def delete():
print("Deleting user: Hiro Hamada")
@app.command()
def create():
print("Creating user: Hiro Hamada")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/commands/index/tutorial005_py310.py
================================================
import typer
app = typer.Typer(suggest_commands=True)
@app.command()
def create():
typer.echo("Creating...")
@app.command()
def delete():
typer.echo("Deleting...")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/commands/name/__init__.py
================================================
================================================
FILE: docs_src/commands/name/tutorial001_py310.py
================================================
import typer
app = typer.Typer()
@app.command("create")
def cli_create_user(username: str):
print(f"Creating user: {username}")
@app.command("delete")
def cli_delete_user(username: str):
print(f"Deleting user: {username}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/commands/one_or_multiple/__init__.py
================================================
================================================
FILE: docs_src/commands/one_or_multiple/tutorial001_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def create():
print("Creating user: Hiro Hamada")
@app.callback()
def callback():
pass
if __name__ == "__main__":
app()
================================================
FILE: docs_src/commands/one_or_multiple/tutorial002_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def create():
print("Creating user: Hiro Hamada")
@app.callback()
def callback():
"""
Creates a single user Hiro Hamada.
In the next version it will create 5 more users.
"""
if __name__ == "__main__":
app()
================================================
FILE: docs_src/commands/options/__init__.py
================================================
================================================
FILE: docs_src/commands/options/tutorial001_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def create(username: str):
print(f"Creating user: {username}")
@app.command()
def delete(
username: str,
force: Annotated[
bool, typer.Option(prompt="Are you sure you want to delete the user?")
],
):
if force:
print(f"Deleting user: {username}")
else:
print("Operation cancelled")
@app.command()
def delete_all(
force: Annotated[
bool, typer.Option(prompt="Are you sure you want to delete ALL users?")
],
):
if force:
print("Deleting all users")
else:
print("Operation cancelled")
@app.command()
def init():
print("Initializing user database")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/commands/options/tutorial001_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def create(username: str):
print(f"Creating user: {username}")
@app.command()
def delete(
username: str,
force: bool = typer.Option(..., prompt="Are you sure you want to delete the user?"),
):
if force:
print(f"Deleting user: {username}")
else:
print("Operation cancelled")
@app.command()
def delete_all(
force: bool = typer.Option(
..., prompt="Are you sure you want to delete ALL users?"
),
):
if force:
print("Deleting all users")
else:
print("Operation cancelled")
@app.command()
def init():
print("Initializing user database")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/exceptions/__init__.py
================================================
================================================
FILE: docs_src/exceptions/tutorial001_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(name: str = "morty"):
print(name + 3)
if __name__ == "__main__":
app()
================================================
FILE: docs_src/exceptions/tutorial002_py310.py
================================================
import typer
app = typer.Typer(pretty_exceptions_show_locals=True)
@app.command()
def main(name: str = "morty"):
print(name + 3)
if __name__ == "__main__":
app()
================================================
FILE: docs_src/exceptions/tutorial003_py310.py
================================================
import typer
app = typer.Typer(pretty_exceptions_short=False)
@app.command()
def main(name: str = "morty"):
print(name + 3)
if __name__ == "__main__":
app()
================================================
FILE: docs_src/exceptions/tutorial004_py310.py
================================================
import typer
app = typer.Typer(pretty_exceptions_enable=False)
@app.command()
def main(name: str = "morty"):
print(name + 3)
if __name__ == "__main__":
app()
================================================
FILE: docs_src/first_steps/__init__.py
================================================
================================================
FILE: docs_src/first_steps/tutorial001_py310.py
================================================
import typer
def main():
print("Hello World")
if __name__ == "__main__":
typer.run(main)
================================================
FILE: docs_src/first_steps/tutorial002_py310.py
================================================
import typer
def main(name: str):
print(f"Hello {name}")
if __name__ == "__main__":
typer.run(main)
================================================
FILE: docs_src/first_steps/tutorial003_py310.py
================================================
import typer
def main(name: str, lastname: str):
print(f"Hello {name} {lastname}")
if __name__ == "__main__":
typer.run(main)
================================================
FILE: docs_src/first_steps/tutorial004_py310.py
================================================
import typer
def main(name: str, lastname: str, formal: bool = False):
if formal:
print(f"Good day Ms. {name} {lastname}.")
else:
print(f"Hello {name} {lastname}")
if __name__ == "__main__":
typer.run(main)
================================================
FILE: docs_src/first_steps/tutorial005_py310.py
================================================
import typer
def main(name: str, lastname: str = "", formal: bool = False):
if formal:
print(f"Good day Ms. {name} {lastname}.")
else:
print(f"Hello {name} {lastname}")
if __name__ == "__main__":
typer.run(main)
================================================
FILE: docs_src/first_steps/tutorial006_py310.py
================================================
import typer
def main(name: str, lastname: str = "", formal: bool = False):
"""
Say hi to NAME, optionally with a --lastname.
If --formal is used, say hi very formally.
"""
if formal:
print(f"Good day Ms. {name} {lastname}.")
else:
print(f"Hello {name} {lastname}")
if __name__ == "__main__":
typer.run(main)
================================================
FILE: docs_src/launch/__init__.py
================================================
================================================
FILE: docs_src/launch/tutorial001_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main():
print("Opening Typer's docs")
typer.launch("https://typer.tiangolo.com")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/launch/tutorial002_py310.py
================================================
from pathlib import Path
import typer
APP_NAME = "my-super-cli-app"
app = typer.Typer()
@app.command()
def main():
app_dir = typer.get_app_dir(APP_NAME)
app_dir_path = Path(app_dir)
app_dir_path.mkdir(parents=True, exist_ok=True)
config_path: Path = Path(app_dir) / "config.json"
if not config_path.is_file():
config_path.write_text('{"version": "1.0.0"}')
config_file_str = str(config_path)
print("Opening config directory")
typer.launch(config_file_str, locate=True)
if __name__ == "__main__":
app()
================================================
FILE: docs_src/multiple_values/__init__.py
================================================
================================================
FILE: docs_src/multiple_values/arguments_with_multiple_values/__init__.py
================================================
================================================
FILE: docs_src/multiple_values/arguments_with_multiple_values/tutorial001_py310.py
================================================
from pathlib import Path
import typer
app = typer.Typer()
@app.command()
def main(files: list[Path], celebration: str):
for path in files:
if path.is_file():
print(f"This file exists: {path.name}")
print(celebration)
if __name__ == "__main__":
app()
================================================
FILE: docs_src/multiple_values/arguments_with_multiple_values/tutorial002_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(
names: Annotated[
tuple[str, str, str], typer.Argument(help="Select 3 characters to play with")
] = ("Harry", "Hermione", "Ron"),
):
for name in names:
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/multiple_values/arguments_with_multiple_values/tutorial002_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(
names: tuple[str, str, str] = typer.Argument(
("Harry", "Hermione", "Ron"), help="Select 3 characters to play with"
),
):
for name in names:
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/multiple_values/multiple_options/__init__.py
================================================
================================================
FILE: docs_src/multiple_values/multiple_options/tutorial001_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(user: Annotated[list[str] | None, typer.Option()] = None):
if not user:
print(f"No provided users (raw input = {user})")
raise typer.Abort()
for u in user:
print(f"Processing user: {u}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/multiple_values/multiple_options/tutorial001_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(user: list[str] | None = typer.Option(None)):
if not user:
print(f"No provided users (raw input = {user})")
raise typer.Abort()
for u in user:
print(f"Processing user: {u}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/multiple_values/multiple_options/tutorial002_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(number: Annotated[list[float], typer.Option()] = []):
print(f"The sum is {sum(number)}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/multiple_values/multiple_options/tutorial002_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(number: list[float] = typer.Option([])):
print(f"The sum is {sum(number)}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/multiple_values/options_with_multiple_values/__init__.py
================================================
================================================
FILE: docs_src/multiple_values/options_with_multiple_values/tutorial001_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(user: Annotated[tuple[str, int, bool], typer.Option()] = (None, None, None)):
username, coins, is_wizard = user
if not username:
print("No user provided")
raise typer.Abort()
print(f"The username {username} has {coins} coins")
if is_wizard:
print("And this user is a wizard!")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/multiple_values/options_with_multiple_values/tutorial001_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(user: tuple[str, int, bool] = typer.Option((None, None, None))):
username, coins, is_wizard = user
if not username:
print("No user provided")
raise typer.Abort()
print(f"The username {username} has {coins} coins")
if is_wizard:
print("And this user is a wizard!")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/one_file_per_command/__init__.py
================================================
================================================
FILE: docs_src/one_file_per_command/app_py310/__init__.py
================================================
================================================
FILE: docs_src/one_file_per_command/app_py310/main.py
================================================
import typer
from .users import app as users_app
from .version import app as version_app
app = typer.Typer()
app.add_typer(version_app)
app.add_typer(users_app, name="users")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/one_file_per_command/app_py310/users/__init__.py
================================================
import typer
from .add import app as add_app
from .delete import app as delete_app
app = typer.Typer()
app.add_typer(add_app)
app.add_typer(delete_app)
================================================
FILE: docs_src/one_file_per_command/app_py310/users/add.py
================================================
import typer
app = typer.Typer()
@app.command()
def add(name: str):
print(f"Adding user: {name}")
================================================
FILE: docs_src/one_file_per_command/app_py310/users/delete.py
================================================
import typer
app = typer.Typer()
@app.command()
def delete(name: str):
print(f"Deleting user: {name}")
================================================
FILE: docs_src/one_file_per_command/app_py310/version.py
================================================
import typer
app = typer.Typer()
@app.command()
def version():
print("My CLI Version 1.0")
================================================
FILE: docs_src/options/__init__.py
================================================
================================================
FILE: docs_src/options/callback/__init__.py
================================================
================================================
FILE: docs_src/options/callback/tutorial001_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
def name_callback(value: str):
if value != "Camila":
raise typer.BadParameter("Only Camila is allowed")
return value
@app.command()
def main(name: Annotated[str | None, typer.Option(callback=name_callback)] = None):
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/callback/tutorial001_py310.py
================================================
import typer
app = typer.Typer()
def name_callback(value: str):
if value != "Camila":
raise typer.BadParameter("Only Camila is allowed")
return value
@app.command()
def main(name: str | None = typer.Option(default=None, callback=name_callback)):
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/callback/tutorial002_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
def name_callback(value: str):
print("Validating name")
if value != "Camila":
raise typer.BadParameter("Only Camila is allowed")
return value
@app.command()
def main(name: Annotated[str | None, typer.Option(callback=name_callback)] = None):
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/callback/tutorial002_py310.py
================================================
import typer
app = typer.Typer()
def name_callback(value: str):
print("Validating name")
if value != "Camila":
raise typer.BadParameter("Only Camila is allowed")
return value
@app.command()
def main(name: str | None = typer.Option(default=None, callback=name_callback)):
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/callback/tutorial003_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
def name_callback(ctx: typer.Context, value: str):
if ctx.resilient_parsing:
return
print("Validating name")
if value != "Camila":
raise typer.BadParameter("Only Camila is allowed")
return value
@app.command()
def main(name: Annotated[str | None, typer.Option(callback=name_callback)] = None):
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/callback/tutorial003_py310.py
================================================
import typer
app = typer.Typer()
def name_callback(ctx: typer.Context, value: str):
if ctx.resilient_parsing:
return
print("Validating name")
if value != "Camila":
raise typer.BadParameter("Only Camila is allowed")
return value
@app.command()
def main(name: str | None = typer.Option(default=None, callback=name_callback)):
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/callback/tutorial004_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
def name_callback(ctx: typer.Context, param: typer.CallbackParam, value: str):
if ctx.resilient_parsing:
return
print(f"Validating param: {param.name}")
if value != "Camila":
raise typer.BadParameter("Only Camila is allowed")
return value
@app.command()
def main(name: Annotated[str | None, typer.Option(callback=name_callback)] = None):
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/callback/tutorial004_py310.py
================================================
import typer
app = typer.Typer()
def name_callback(ctx: typer.Context, param: typer.CallbackParam, value: str):
if ctx.resilient_parsing:
return
print(f"Validating param: {param.name}")
if value != "Camila":
raise typer.BadParameter("Only Camila is allowed")
return value
@app.command()
def main(name: str | None = typer.Option(default=None, callback=name_callback)):
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/help/__init__.py
================================================
================================================
FILE: docs_src/options/help/tutorial001_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(
name: str,
lastname: Annotated[str, typer.Option(help="Last name of person to greet.")] = "",
formal: Annotated[bool, typer.Option(help="Say hi formally.")] = False,
):
"""
Say hi to NAME, optionally with a --lastname.
If --formal is used, say hi very formally.
"""
if formal:
print(f"Good day Ms. {name} {lastname}.")
else:
print(f"Hello {name} {lastname}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/help/tutorial001_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(
name: str,
lastname: str = typer.Option("", help="Last name of person to greet."),
formal: bool = typer.Option(False, help="Say hi formally."),
):
"""
Say hi to NAME, optionally with a --lastname.
If --formal is used, say hi very formally.
"""
if formal:
print(f"Good day Ms. {name} {lastname}.")
else:
print(f"Hello {name} {lastname}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/help/tutorial002_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(
name: str,
lastname: Annotated[str, typer.Option(help="Last name of person to greet.")] = "",
formal: Annotated[
bool,
typer.Option(
help="Say hi formally.", rich_help_panel="Customization and Utils"
),
] = False,
debug: Annotated[
bool,
typer.Option(
help="Enable debugging.", rich_help_panel="Customization and Utils"
),
] = False,
):
"""
Say hi to NAME, optionally with a --lastname.
If --formal is used, say hi very formally.
"""
if formal:
print(f"Good day Ms. {name} {lastname}.")
else:
print(f"Hello {name} {lastname}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/help/tutorial002_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(
name: str,
lastname: str = typer.Option("", help="Last name of person to greet."),
formal: bool = typer.Option(
False, help="Say hi formally.", rich_help_panel="Customization and Utils"
),
debug: bool = typer.Option(
False, help="Enable debugging.", rich_help_panel="Customization and Utils"
),
):
"""
Say hi to NAME, optionally with a --lastname.
If --formal is used, say hi very formally.
"""
if formal:
print(f"Good day Ms. {name} {lastname}.")
else:
print(f"Hello {name} {lastname}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/help/tutorial003_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(fullname: Annotated[str, typer.Option(show_default=False)] = "Wade Wilson"):
print(f"Hello {fullname}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/help/tutorial003_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(fullname: str = typer.Option("Wade Wilson", show_default=False)):
print(f"Hello {fullname}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/help/tutorial004_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(
fullname: Annotated[
str, typer.Option(show_default="Deadpoolio the amazing's name")
] = "Wade Wilson",
):
print(f"Hello {fullname}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/help/tutorial004_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(
fullname: str = typer.Option(
"Wade Wilson", show_default="Deadpoolio the amazing's name"
),
):
print(f"Hello {fullname}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/name/__init__.py
================================================
================================================
FILE: docs_src/options/name/tutorial001_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(user_name: Annotated[str, typer.Option("--name")]):
print(f"Hello {user_name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/name/tutorial001_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(user_name: str = typer.Option(..., "--name")):
print(f"Hello {user_name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/name/tutorial002_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(user_name: Annotated[str, typer.Option("--name", "-n")]):
print(f"Hello {user_name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/name/tutorial002_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(user_name: str = typer.Option(..., "--name", "-n")):
print(f"Hello {user_name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/name/tutorial003_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(user_name: Annotated[str, typer.Option("-n")]):
print(f"Hello {user_name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/name/tutorial003_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(user_name: str = typer.Option(..., "-n")):
print(f"Hello {user_name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/name/tutorial004_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(user_name: Annotated[str, typer.Option("--user-name", "-n")]):
print(f"Hello {user_name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/name/tutorial004_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(user_name: str = typer.Option(..., "--user-name", "-n")):
print(f"Hello {user_name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/name/tutorial005_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(
name: Annotated[str, typer.Option("--name", "-n")],
formal: Annotated[bool, typer.Option("--formal", "-f")] = False,
):
if formal:
print(f"Good day Ms. {name}.")
else:
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/name/tutorial005_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(
name: str = typer.Option(..., "--name", "-n"),
formal: bool = typer.Option(False, "--formal", "-f"),
):
if formal:
print(f"Good day Ms. {name}.")
else:
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/password/__init__.py
================================================
================================================
FILE: docs_src/options/password/tutorial001_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(
name: str,
email: Annotated[str, typer.Option(prompt=True, confirmation_prompt=True)],
):
print(f"Hello {name}, your email is {email}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/password/tutorial001_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(
name: str, email: str = typer.Option(..., prompt=True, confirmation_prompt=True)
):
print(f"Hello {name}, your email is {email}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/password/tutorial002_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(
name: str,
password: Annotated[
str, typer.Option(prompt=True, confirmation_prompt=True, hide_input=True)
],
):
print(f"Hello {name}. Doing something very secure with password.")
print(f"...just kidding, here it is, very insecure: {password}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/password/tutorial002_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(
name: str,
password: str = typer.Option(
..., prompt=True, confirmation_prompt=True, hide_input=True
),
):
print(f"Hello {name}. Doing something very secure with password.")
print(f"...just kidding, here it is, very insecure: {password}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/prompt/__init__.py
================================================
================================================
FILE: docs_src/options/prompt/tutorial001_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(name: str, lastname: Annotated[str, typer.Option(prompt=True)]):
print(f"Hello {name} {lastname}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/prompt/tutorial001_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(name: str, lastname: str = typer.Option(..., prompt=True)):
print(f"Hello {name} {lastname}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/prompt/tutorial002_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(
name: str,
lastname: Annotated[str, typer.Option(prompt="Please tell me your last name")],
):
print(f"Hello {name} {lastname}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/prompt/tutorial002_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(
name: str, lastname: str = typer.Option(..., prompt="Please tell me your last name")
):
print(f"Hello {name} {lastname}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/prompt/tutorial003_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(
project_name: Annotated[str, typer.Option(prompt=True, confirmation_prompt=True)],
):
print(f"Deleting project {project_name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/prompt/tutorial003_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(project_name: str = typer.Option(..., prompt=True, confirmation_prompt=True)):
print(f"Deleting project {project_name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/required/__init__.py
================================================
================================================
FILE: docs_src/options/required/tutorial001_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(name: str, lastname: Annotated[str, typer.Option()]):
print(f"Hello {name} {lastname}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/required/tutorial001_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(name: str, lastname: str = typer.Option()):
print(f"Hello {name} {lastname}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/required/tutorial002_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(name: str, lastname: str = typer.Option(default=...)):
print(f"Hello {name} {lastname}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/version/__init__.py
================================================
================================================
FILE: docs_src/options/version/tutorial001_an_py310.py
================================================
from typing import Annotated
import typer
__version__ = "0.1.0"
app = typer.Typer()
def version_callback(value: bool):
if value:
print(f"Awesome CLI Version: {__version__}")
raise typer.Exit()
@app.command()
def main(
name: Annotated[str, typer.Option()] = "World",
version: Annotated[
bool | None, typer.Option("--version", callback=version_callback)
] = None,
):
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/version/tutorial001_py310.py
================================================
import typer
__version__ = "0.1.0"
app = typer.Typer()
def version_callback(value: bool):
if value:
print(f"Awesome CLI Version: {__version__}")
raise typer.Exit()
@app.command()
def main(
name: str = typer.Option("World"),
version: bool | None = typer.Option(None, "--version", callback=version_callback),
):
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/version/tutorial002_an_py310.py
================================================
from typing import Annotated
import typer
__version__ = "0.1.0"
app = typer.Typer()
def version_callback(value: bool):
if value:
print(f"Awesome CLI Version: {__version__}")
raise typer.Exit()
def name_callback(name: str):
if name != "Camila":
raise typer.BadParameter("Only Camila is allowed")
return name
@app.command()
def main(
name: Annotated[str, typer.Option(callback=name_callback)],
version: Annotated[
bool | None, typer.Option("--version", callback=version_callback)
] = None,
):
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/version/tutorial002_py310.py
================================================
import typer
__version__ = "0.1.0"
app = typer.Typer()
def version_callback(value: bool):
if value:
print(f"Awesome CLI Version: {__version__}")
raise typer.Exit()
def name_callback(name: str):
if name != "Camila":
raise typer.BadParameter("Only Camila is allowed")
return name
@app.command()
def main(
name: str = typer.Option(..., callback=name_callback),
version: bool | None = typer.Option(None, "--version", callback=version_callback),
):
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/version/tutorial003_an_py310.py
================================================
from typing import Annotated
import typer
__version__ = "0.1.0"
app = typer.Typer()
def version_callback(value: bool):
if value:
print(f"Awesome CLI Version: {__version__}")
raise typer.Exit()
def name_callback(name: str):
if name != "Camila":
raise typer.BadParameter("Only Camila is allowed")
return name
@app.command()
def main(
name: Annotated[str, typer.Option(callback=name_callback)],
version: Annotated[
bool | None,
typer.Option("--version", callback=version_callback, is_eager=True),
] = None,
):
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options/version/tutorial003_py310.py
================================================
import typer
__version__ = "0.1.0"
app = typer.Typer()
def version_callback(value: bool):
if value:
print(f"Awesome CLI Version: {__version__}")
raise typer.Exit()
def name_callback(name: str):
if name != "Camila":
raise typer.BadParameter("Only Camila is allowed")
return name
@app.command()
def main(
name: str = typer.Option(..., callback=name_callback),
version: bool | None = typer.Option(
None, "--version", callback=version_callback, is_eager=True
),
):
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options_autocompletion/__init__.py
================================================
================================================
FILE: docs_src/options_autocompletion/tutorial001_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(name: Annotated[str, typer.Option(help="The name to say hi to.")] = "World"):
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options_autocompletion/tutorial001_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(name: str = typer.Option("World", help="The name to say hi to.")):
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options_autocompletion/tutorial002_an_py310.py
================================================
from typing import Annotated
import typer
def complete_name():
return ["Camila", "Carlos", "Sebastian"]
app = typer.Typer()
@app.command()
def main(
name: Annotated[
str, typer.Option(help="The name to say hi to.", autocompletion=complete_name)
] = "World",
):
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options_autocompletion/tutorial002_py310.py
================================================
import typer
def complete_name():
return ["Camila", "Carlos", "Sebastian"]
app = typer.Typer()
@app.command()
def main(
name: str = typer.Option(
"World", help="The name to say hi to.", autocompletion=complete_name
),
):
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options_autocompletion/tutorial003_an_py310.py
================================================
from typing import Annotated
import typer
valid_names = ["Camila", "Carlos", "Sebastian"]
def complete_name(incomplete: str):
completion = []
for name in valid_names:
if name.startswith(incomplete):
completion.append(name)
return completion
app = typer.Typer()
@app.command()
def main(
name: Annotated[
str, typer.Option(help="The name to say hi to.", autocompletion=complete_name)
] = "World",
):
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options_autocompletion/tutorial003_py310.py
================================================
import typer
valid_names = ["Camila", "Carlos", "Sebastian"]
def complete_name(incomplete: str):
completion = []
for name in valid_names:
if name.startswith(incomplete):
completion.append(name)
return completion
app = typer.Typer()
@app.command()
def main(
name: str = typer.Option(
"World", help="The name to say hi to.", autocompletion=complete_name
),
):
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options_autocompletion/tutorial004_an_py310.py
================================================
from typing import Annotated
import typer
valid_completion_items = [
("Camila", "The reader of books."),
("Carlos", "The writer of scripts."),
("Sebastian", "The type hints guy."),
]
def complete_name(incomplete: str):
completion = []
for name, help_text in valid_completion_items:
if name.startswith(incomplete):
completion_item = (name, help_text)
completion.append(completion_item)
return completion
app = typer.Typer()
@app.command()
def main(
name: Annotated[
str, typer.Option(help="The name to say hi to.", autocompletion=complete_name)
] = "World",
):
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options_autocompletion/tutorial004_py310.py
================================================
import typer
valid_completion_items = [
("Camila", "The reader of books."),
("Carlos", "The writer of scripts."),
("Sebastian", "The type hints guy."),
]
def complete_name(incomplete: str):
completion = []
for name, help_text in valid_completion_items:
if name.startswith(incomplete):
completion_item = (name, help_text)
completion.append(completion_item)
return completion
app = typer.Typer()
@app.command()
def main(
name: str = typer.Option(
"World", help="The name to say hi to.", autocompletion=complete_name
),
):
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options_autocompletion/tutorial005_an_py310.py
================================================
from typing import Annotated
import typer
valid_completion_items = [
("Camila", "The reader of books."),
("Carlos", "The writer of scripts."),
("Sebastian", "The type hints guy."),
]
def complete_name(incomplete: str):
for name, help_text in valid_completion_items:
if name.startswith(incomplete):
yield (name, help_text)
app = typer.Typer()
@app.command()
def main(
name: Annotated[
str, typer.Option(help="The name to say hi to.", autocompletion=complete_name)
] = "World",
):
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options_autocompletion/tutorial005_py310.py
================================================
import typer
valid_completion_items = [
("Camila", "The reader of books."),
("Carlos", "The writer of scripts."),
("Sebastian", "The type hints guy."),
]
def complete_name(incomplete: str):
for name, help_text in valid_completion_items:
if name.startswith(incomplete):
yield (name, help_text)
app = typer.Typer()
@app.command()
def main(
name: str = typer.Option(
"World", help="The name to say hi to.", autocompletion=complete_name
),
):
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options_autocompletion/tutorial006_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(
name: Annotated[list[str], typer.Option(help="The name to say hi to.")] = ["World"],
):
for each_name in name:
print(f"Hello {each_name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options_autocompletion/tutorial006_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(name: list[str] = typer.Option(["World"], help="The name to say hi to.")):
for each_name in name:
print(f"Hello {each_name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options_autocompletion/tutorial007_an_py310.py
================================================
from typing import Annotated
import typer
valid_completion_items = [
("Camila", "The reader of books."),
("Carlos", "The writer of scripts."),
("Sebastian", "The type hints guy."),
]
def complete_name(ctx: typer.Context, incomplete: str):
names = ctx.params.get("name") or []
for name, help_text in valid_completion_items:
if name.startswith(incomplete) and name not in names:
yield (name, help_text)
app = typer.Typer()
@app.command()
def main(
name: Annotated[
list[str],
typer.Option(help="The name to say hi to.", autocompletion=complete_name),
] = ["World"],
):
for n in name:
print(f"Hello {n}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options_autocompletion/tutorial007_py310.py
================================================
import typer
valid_completion_items = [
("Camila", "The reader of books."),
("Carlos", "The writer of scripts."),
("Sebastian", "The type hints guy."),
]
def complete_name(ctx: typer.Context, incomplete: str):
names = ctx.params.get("name") or []
for name, help_text in valid_completion_items:
if name.startswith(incomplete) and name not in names:
yield (name, help_text)
app = typer.Typer()
@app.command()
def main(
name: list[str] = typer.Option(
["World"], help="The name to say hi to.", autocompletion=complete_name
),
):
for n in name:
print(f"Hello {n}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options_autocompletion/tutorial008_an_py310.py
================================================
from typing import Annotated
import typer
from rich.console import Console
valid_completion_items = [
("Camila", "The reader of books."),
("Carlos", "The writer of scripts."),
("Sebastian", "The type hints guy."),
]
err_console = Console(stderr=True)
def complete_name(args: list[str], incomplete: str):
err_console.print(f"{args}")
for name, help_text in valid_completion_items:
if name.startswith(incomplete):
yield (name, help_text)
app = typer.Typer()
@app.command()
def main(
name: Annotated[
list[str],
typer.Option(help="The name to say hi to.", autocompletion=complete_name),
] = ["World"],
):
for n in name:
print(f"Hello {n}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options_autocompletion/tutorial008_py310.py
================================================
import typer
from rich.console import Console
valid_completion_items = [
("Camila", "The reader of books."),
("Carlos", "The writer of scripts."),
("Sebastian", "The type hints guy."),
]
err_console = Console(stderr=True)
def complete_name(args: list[str], incomplete: str):
err_console.print(f"{args}")
for name, help_text in valid_completion_items:
if name.startswith(incomplete):
yield (name, help_text)
app = typer.Typer()
@app.command()
def main(
name: list[str] = typer.Option(
["World"], help="The name to say hi to.", autocompletion=complete_name
),
):
for n in name:
print(f"Hello {n}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options_autocompletion/tutorial009_an_py310.py
================================================
from typing import Annotated
import typer
from rich.console import Console
valid_completion_items = [
("Camila", "The reader of books."),
("Carlos", "The writer of scripts."),
("Sebastian", "The type hints guy."),
]
err_console = Console(stderr=True)
def complete_name(ctx: typer.Context, args: list[str], incomplete: str):
err_console.print(f"{args}")
names = ctx.params.get("name") or []
for name, help_text in valid_completion_items:
if name.startswith(incomplete) and name not in names:
yield (name, help_text)
app = typer.Typer()
@app.command()
def main(
name: Annotated[
list[str],
typer.Option(help="The name to say hi to.", autocompletion=complete_name),
] = ["World"],
):
for n in name:
print(f"Hello {n}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/options_autocompletion/tutorial009_py310.py
================================================
import typer
from rich.console import Console
valid_completion_items = [
("Camila", "The reader of books."),
("Carlos", "The writer of scripts."),
("Sebastian", "The type hints guy."),
]
err_console = Console(stderr=True)
def complete_name(ctx: typer.Context, args: list[str], incomplete: str):
err_console.print(f"{args}")
names = ctx.params.get("name") or []
for name, help_text in valid_completion_items:
if name.startswith(incomplete) and name not in names:
yield (name, help_text)
app = typer.Typer()
@app.command()
def main(
name: list[str] = typer.Option(
["World"], help="The name to say hi to.", autocompletion=complete_name
),
):
for n in name:
print(f"Hello {n}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/__init__.py
================================================
================================================
FILE: docs_src/parameter_types/bool/__init__.py
================================================
================================================
FILE: docs_src/parameter_types/bool/tutorial001_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(force: Annotated[bool, typer.Option("--force")] = False):
if force:
print("Forcing operation")
else:
print("Not forcing")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/bool/tutorial001_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(force: bool = typer.Option(False, "--force")):
if force:
print("Forcing operation")
else:
print("Not forcing")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/bool/tutorial002_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(accept: Annotated[bool | None, typer.Option("--accept/--reject")] = None):
if accept is None:
print("I don't know what you want yet")
elif accept:
print("Accepting!")
else:
print("Rejecting!")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/bool/tutorial002_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(accept: bool | None = typer.Option(None, "--accept/--reject")):
if accept is None:
print("I don't know what you want yet")
elif accept:
print("Accepting!")
else:
print("Rejecting!")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/bool/tutorial003_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(force: Annotated[bool, typer.Option("--force/--no-force", "-f/-F")] = False):
if force:
print("Forcing operation")
else:
print("Not forcing")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/bool/tutorial003_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(force: bool = typer.Option(False, "--force/--no-force", "-f/-F")):
if force:
print("Forcing operation")
else:
print("Not forcing")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/bool/tutorial004_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(in_prod: Annotated[bool, typer.Option(" /--demo", " /-d")] = True):
if in_prod:
print("Running in production")
else:
print("Running demo")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/bool/tutorial004_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(in_prod: bool = typer.Option(True, " /--demo", " /-d")):
if in_prod:
print("Running in production")
else:
print("Running demo")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/custom_types/__init__.py
================================================
================================================
FILE: docs_src/parameter_types/custom_types/tutorial001_an_py310.py
================================================
from typing import Annotated
import typer
class CustomClass:
def __init__(self, value: str):
self.value = value
def __str__(self):
return f""
def parse_custom_class(value: str):
return CustomClass(value * 2)
app = typer.Typer()
@app.command()
def main(
custom_arg: Annotated[CustomClass, typer.Argument(parser=parse_custom_class)],
custom_opt: Annotated[CustomClass, typer.Option(parser=parse_custom_class)] = "Foo",
):
print(f"custom_arg is {custom_arg}")
print(f"--custom-opt is {custom_opt}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/custom_types/tutorial001_py310.py
================================================
import typer
class CustomClass:
def __init__(self, value: str):
self.value = value
def __str__(self):
return f""
def parse_custom_class(value: str):
return CustomClass(value * 2)
app = typer.Typer()
@app.command()
def main(
custom_arg: CustomClass = typer.Argument(parser=parse_custom_class),
custom_opt: CustomClass = typer.Option("Foo", parser=parse_custom_class),
):
print(f"custom_arg is {custom_arg}")
print(f"--custom-opt is {custom_opt}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/datetime/__init__.py
================================================
================================================
FILE: docs_src/parameter_types/datetime/tutorial001_py310.py
================================================
from datetime import datetime
import typer
app = typer.Typer()
@app.command()
def main(birth: datetime):
print(f"Interesting day to be born: {birth}")
print(f"Birth hour: {birth.hour}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/datetime/tutorial002_an_py310.py
================================================
from datetime import datetime
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(
launch_date: Annotated[
datetime,
typer.Argument(
formats=["%Y-%m-%d", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S", "%m/%d/%Y"]
),
],
):
print(f"Launch will be at: {launch_date}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/datetime/tutorial002_py310.py
================================================
from datetime import datetime
import typer
app = typer.Typer()
@app.command()
def main(
launch_date: datetime = typer.Argument(
..., formats=["%Y-%m-%d", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S", "%m/%d/%Y"]
),
):
print(f"Launch will be at: {launch_date}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/enum/__init__.py
================================================
================================================
FILE: docs_src/parameter_types/enum/tutorial001_py310.py
================================================
from enum import Enum
import typer
class NeuralNetwork(str, Enum):
simple = "simple"
conv = "conv"
lstm = "lstm"
app = typer.Typer()
@app.command()
def main(network: NeuralNetwork = NeuralNetwork.simple):
print(f"Training neural network of type: {network.value}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/enum/tutorial002_an_py310.py
================================================
from enum import Enum
from typing import Annotated
import typer
class NeuralNetwork(str, Enum):
simple = "simple"
conv = "conv"
lstm = "lstm"
app = typer.Typer()
@app.command()
def main(
network: Annotated[
NeuralNetwork, typer.Option(case_sensitive=False)
] = NeuralNetwork.simple,
):
print(f"Training neural network of type: {network.value}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/enum/tutorial002_py310.py
================================================
from enum import Enum
import typer
class NeuralNetwork(str, Enum):
simple = "simple"
conv = "conv"
lstm = "lstm"
app = typer.Typer()
@app.command()
def main(
network: NeuralNetwork = typer.Option(NeuralNetwork.simple, case_sensitive=False),
):
print(f"Training neural network of type: {network.value}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/enum/tutorial003_an_py310.py
================================================
from enum import Enum
from typing import Annotated
import typer
class Food(str, Enum):
food_1 = "Eggs"
food_2 = "Bacon"
food_3 = "Cheese"
app = typer.Typer()
@app.command()
def main(groceries: Annotated[list[Food], typer.Option()] = [Food.food_1, Food.food_3]):
print(f"Buying groceries: {', '.join([f.value for f in groceries])}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/enum/tutorial003_py310.py
================================================
from enum import Enum
import typer
class Food(str, Enum):
food_1 = "Eggs"
food_2 = "Bacon"
food_3 = "Cheese"
app = typer.Typer()
@app.command()
def main(groceries: list[Food] = typer.Option([Food.food_1, Food.food_3])):
print(f"Buying groceries: {', '.join([f.value for f in groceries])}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/enum/tutorial004_an_py310.py
================================================
from typing import Annotated, Literal
import typer
app = typer.Typer()
@app.command()
def main(
network: Annotated[Literal["simple", "conv", "lstm"], typer.Option()] = "simple",
):
print(f"Training neural network of type: {network}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/enum/tutorial004_py310.py
================================================
from typing import Literal
import typer
app = typer.Typer()
@app.command()
def main(network: Literal["simple", "conv", "lstm"] = typer.Option("simple")):
print(f"Training neural network of type: {network}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/file/__init__.py
================================================
================================================
FILE: docs_src/parameter_types/file/tutorial001_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(config: Annotated[typer.FileText, typer.Option()]):
for line in config:
print(f"Config line: {line}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/file/tutorial001_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(config: typer.FileText = typer.Option(...)):
for line in config:
print(f"Config line: {line}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/file/tutorial002_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(config: Annotated[typer.FileTextWrite, typer.Option()]):
config.write("Some config written by the app")
print("Config written")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/file/tutorial002_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(config: typer.FileTextWrite = typer.Option(...)):
config.write("Some config written by the app")
print("Config written")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/file/tutorial003_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(file: Annotated[typer.FileBinaryRead, typer.Option()]):
processed_total = 0
for bytes_chunk in file:
# Process the bytes in bytes_chunk
processed_total += len(bytes_chunk)
print(f"Processed bytes total: {processed_total}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/file/tutorial003_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(file: typer.FileBinaryRead = typer.Option(...)):
processed_total = 0
for bytes_chunk in file:
# Process the bytes in bytes_chunk
processed_total += len(bytes_chunk)
print(f"Processed bytes total: {processed_total}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/file/tutorial004_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(file: Annotated[typer.FileBinaryWrite, typer.Option()]):
first_line_str = "some settings\n"
# You cannot write str directly to a binary file, you have to encode it to get bytes
first_line_bytes = first_line_str.encode("utf-8")
# Then you can write the bytes
file.write(first_line_bytes)
# This is already bytes, it starts with b"
second_line = b"la cig\xc3\xbce\xc3\xb1a trae al ni\xc3\xb1o"
file.write(second_line)
print("Binary file written")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/file/tutorial004_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(file: typer.FileBinaryWrite = typer.Option(...)):
first_line_str = "some settings\n"
# You cannot write str directly to a binary file, you have to encode it to get bytes
first_line_bytes = first_line_str.encode("utf-8")
# Then you can write the bytes
file.write(first_line_bytes)
# This is already bytes, it starts with b"
second_line = b"la cig\xc3\xbce\xc3\xb1a trae al ni\xc3\xb1o"
file.write(second_line)
print("Binary file written")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/file/tutorial005_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(config: Annotated[typer.FileText, typer.Option(mode="a")]):
config.write("This is a single line\n")
print("Config line written")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/file/tutorial005_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(config: typer.FileText = typer.Option(..., mode="a")):
config.write("This is a single line\n")
print("Config line written")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/index/__init__.py
================================================
================================================
FILE: docs_src/parameter_types/index/tutorial001_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(name: str, age: int = 20, height_meters: float = 1.89, female: bool = True):
print(f"NAME is {name}, of type: {type(name)}")
print(f"--age is {age}, of type: {type(age)}")
print(f"--height-meters is {height_meters}, of type: {type(height_meters)}")
print(f"--female is {female}, of type: {type(female)}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/number/__init__.py
================================================
================================================
FILE: docs_src/parameter_types/number/tutorial001_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(
id: Annotated[int, typer.Argument(min=0, max=1000)],
age: Annotated[int, typer.Option(min=18)] = 20,
score: Annotated[float, typer.Option(max=100)] = 0,
):
print(f"ID is {id}")
print(f"--age is {age}")
print(f"--score is {score}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/number/tutorial001_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(
id: int = typer.Argument(..., min=0, max=1000),
age: int = typer.Option(20, min=18),
score: float = typer.Option(0, max=100),
):
print(f"ID is {id}")
print(f"--age is {age}")
print(f"--score is {score}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/number/tutorial002_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(
id: Annotated[int, typer.Argument(min=0, max=1000)],
rank: Annotated[int, typer.Option(max=10, clamp=True)] = 0,
score: Annotated[float, typer.Option(min=0, max=100, clamp=True)] = 0,
):
print(f"ID is {id}")
print(f"--rank is {rank}")
print(f"--score is {score}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/number/tutorial002_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(
id: int = typer.Argument(..., min=0, max=1000),
rank: int = typer.Option(0, max=10, clamp=True),
score: float = typer.Option(0, min=0, max=100, clamp=True),
):
print(f"ID is {id}")
print(f"--rank is {rank}")
print(f"--score is {score}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/number/tutorial003_an_py310.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(verbose: Annotated[int, typer.Option("--verbose", "-v", count=True)] = 0):
print(f"Verbose level is {verbose}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/number/tutorial003_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(verbose: int = typer.Option(0, "--verbose", "-v", count=True)):
print(f"Verbose level is {verbose}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/path/__init__.py
================================================
================================================
FILE: docs_src/parameter_types/path/tutorial001_an_py310.py
================================================
from pathlib import Path
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(config: Annotated[Path | None, typer.Option()] = None):
if config is None:
print("No config file")
raise typer.Abort()
if config.is_file():
text = config.read_text()
print(f"Config file contents: {text}")
elif config.is_dir():
print("Config is a directory, will use all its config files")
elif not config.exists():
print("The config doesn't exist")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/path/tutorial001_py310.py
================================================
from pathlib import Path
import typer
app = typer.Typer()
@app.command()
def main(config: Path | None = typer.Option(None)):
if config is None:
print("No config file")
raise typer.Abort()
if config.is_file():
text = config.read_text()
print(f"Config file contents: {text}")
elif config.is_dir():
print("Config is a directory, will use all its config files")
elif not config.exists():
print("The config doesn't exist")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/path/tutorial002_an_py310.py
================================================
from pathlib import Path
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(
config: Annotated[
Path,
typer.Option(
exists=True,
file_okay=True,
dir_okay=False,
writable=False,
readable=True,
resolve_path=True,
),
],
):
text = config.read_text()
print(f"Config file contents: {text}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/path/tutorial002_py310.py
================================================
from pathlib import Path
import typer
app = typer.Typer()
@app.command()
def main(
config: Path = typer.Option(
...,
exists=True,
file_okay=True,
dir_okay=False,
writable=False,
readable=True,
resolve_path=True,
),
):
text = config.read_text()
print(f"Config file contents: {text}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/parameter_types/uuid/__init__.py
================================================
================================================
FILE: docs_src/parameter_types/uuid/tutorial001_py310.py
================================================
from uuid import UUID
import typer
app = typer.Typer()
@app.command()
def main(user_id: UUID):
print(f"USER_ID is {user_id}")
print(f"UUID version is: {user_id.version}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/printing/__init__.py
================================================
================================================
FILE: docs_src/printing/tutorial001_py310.py
================================================
import typer
from rich import print
data = {
"name": "Rick",
"age": 42,
"items": [{"name": "Portal Gun"}, {"name": "Plumbus"}],
"active": True,
"affiliation": None,
}
app = typer.Typer()
@app.command()
def main():
print("Here's the data")
print(data)
if __name__ == "__main__":
app()
================================================
FILE: docs_src/printing/tutorial002_py310.py
================================================
import typer
from rich import print
app = typer.Typer()
@app.command()
def main():
print("[bold red]Alert![/bold red] [green]Portal gun[/green] shooting! :boom:")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/printing/tutorial003_py310.py
================================================
import typer
from rich.console import Console
from rich.table import Table
console = Console()
app = typer.Typer()
@app.command()
def main():
table = Table("Name", "Item")
table.add_row("Rick", "Portal Gun")
table.add_row("Morty", "Plumbus")
console.print(table)
if __name__ == "__main__":
app()
================================================
FILE: docs_src/printing/tutorial004_py310.py
================================================
import typer
from rich.console import Console
err_console = Console(stderr=True)
app = typer.Typer()
@app.command()
def main():
err_console.print("Here is something written to standard error")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/printing/tutorial005_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(good: bool = True):
message_start = "everything is "
if good:
ending = typer.style("good", fg=typer.colors.GREEN, bold=True)
else:
ending = typer.style("bad", fg=typer.colors.WHITE, bg=typer.colors.RED)
message = message_start + ending
typer.echo(message)
if __name__ == "__main__":
app()
================================================
FILE: docs_src/printing/tutorial006_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(name: str):
typer.secho(f"Welcome here {name}", fg=typer.colors.MAGENTA)
if __name__ == "__main__":
app()
================================================
FILE: docs_src/progressbar/__init__.py
================================================
================================================
FILE: docs_src/progressbar/tutorial001_py310.py
================================================
import time
import typer
from rich.progress import track
app = typer.Typer()
@app.command()
def main():
total = 0
for value in track(range(100), description="Processing..."):
# Fake processing time
time.sleep(0.01)
total += 1
print(f"Processed {total} things.")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/progressbar/tutorial002_py310.py
================================================
import time
import typer
from rich.progress import Progress, SpinnerColumn, TextColumn
app = typer.Typer()
@app.command()
def main():
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
transient=True,
) as progress:
progress.add_task(description="Processing...", total=None)
progress.add_task(description="Preparing...", total=None)
time.sleep(5)
print("Done!")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/progressbar/tutorial003_py310.py
================================================
import time
import typer
app = typer.Typer()
@app.command()
def main():
total = 0
with typer.progressbar(range(100)) as progress:
for value in progress:
# Fake processing time
time.sleep(0.01)
total += 1
print(f"Processed {total} things.")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/progressbar/tutorial004_py310.py
================================================
import time
import typer
def iterate_user_ids():
# Let's imagine this is a web API, not a range()
for i in range(100):
yield i
app = typer.Typer()
@app.command()
def main():
total = 0
with typer.progressbar(iterate_user_ids(), length=100) as progress:
for value in progress:
# Fake processing time
time.sleep(0.01)
total += 1
print(f"Processed {total} user IDs.")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/progressbar/tutorial005_py310.py
================================================
import time
import typer
app = typer.Typer()
@app.command()
def main():
total = 0
with typer.progressbar(range(100), label="Processing") as progress:
for value in progress:
# Fake processing time
time.sleep(0.01)
total += 1
print(f"Processed {total} things.")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/progressbar/tutorial006_py310.py
================================================
import time
import typer
app = typer.Typer()
@app.command()
def main():
total = 1000
with typer.progressbar(length=total) as progress:
for batch in range(4):
# Fake processing time
time.sleep(1)
# Increment by 250 on each loop iteration
# (it will take 4 seconds to reach 1000)
progress.update(250)
print(f"Processed {total} things in batches.")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/prompt/__init__.py
================================================
================================================
FILE: docs_src/prompt/tutorial001_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main():
person_name = typer.prompt("What's your name?")
print(f"Hello {person_name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/prompt/tutorial002_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main():
delete = typer.confirm("Are you sure you want to delete it?")
if not delete:
print("Not deleting")
raise typer.Abort()
print("Deleting it!")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/prompt/tutorial003_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main():
delete = typer.confirm("Are you sure you want to delete it?", abort=True)
print("Deleting it!")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/prompt/tutorial004_py310.py
================================================
import typer
from rich.prompt import Prompt
app = typer.Typer()
@app.command()
def main():
name = Prompt.ask("Enter your name :sunglasses:")
print(f"Hey there {name}!")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/subcommands/__init__.py
================================================
================================================
FILE: docs_src/subcommands/callback_override/__init__.py
================================================
================================================
FILE: docs_src/subcommands/callback_override/tutorial001_py310.py
================================================
import typer
app = typer.Typer()
users_app = typer.Typer()
app.add_typer(users_app, name="users")
@users_app.callback()
def users_callback():
print("Running a users command")
@users_app.command()
def create(name: str):
print(f"Creating user: {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/subcommands/callback_override/tutorial002_py310.py
================================================
import typer
app = typer.Typer()
def users_callback():
print("Running a users command")
users_app = typer.Typer(callback=users_callback)
app.add_typer(users_app, name="users")
@users_app.command()
def create(name: str):
print(f"Creating user: {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/subcommands/callback_override/tutorial003_py310.py
================================================
import typer
app = typer.Typer()
def default_callback():
print("Running a users command")
users_app = typer.Typer(callback=default_callback)
app.add_typer(users_app, name="users")
@users_app.callback()
def user_callback():
print("Callback override, running users command")
@users_app.command()
def create(name: str):
print(f"Creating user: {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/subcommands/callback_override/tutorial004_py310.py
================================================
import typer
app = typer.Typer()
def default_callback():
print("Running a users command")
users_app = typer.Typer(callback=default_callback)
def callback_for_add_typer():
print("I have the high land! Running users command")
app.add_typer(users_app, name="users", callback=callback_for_add_typer)
@users_app.callback()
def user_callback():
print("Callback override, running users command")
@users_app.command()
def create(name: str):
print(f"Creating user: {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/subcommands/name_help/__init__.py
================================================
================================================
FILE: docs_src/subcommands/name_help/tutorial001_py310.py
================================================
import typer
app = typer.Typer()
users_app = typer.Typer()
app.add_typer(users_app, name="users", help="Manage users in the app.")
@users_app.command()
def create(name: str):
print(f"Creating user: {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/subcommands/name_help/tutorial002_py310.py
================================================
import typer
app = typer.Typer()
users_app = typer.Typer()
app.add_typer(users_app, name="users")
@users_app.callback()
def users():
"""
Manage users in the app.
"""
@users_app.command()
def create(name: str):
print(f"Creating user: {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/subcommands/name_help/tutorial003_py310.py
================================================
import typer
app = typer.Typer()
def users():
"""
Manage users in the app.
"""
users_app = typer.Typer(callback=users, name="users")
app.add_typer(users_app)
@users_app.command()
def create(name: str):
print(f"Creating user: {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/subcommands/name_help/tutorial004_py310.py
================================================
import typer
app = typer.Typer()
def old_callback():
"""
Old callback help.
"""
users_app = typer.Typer(callback=old_callback)
app.add_typer(users_app, name="users")
@users_app.callback()
def users():
"""
Manage users in the app.
"""
@users_app.command()
def create(name: str):
print(f"Creating user: {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/subcommands/name_help/tutorial005_py310.py
================================================
import typer
app = typer.Typer()
def old_callback():
"""
Old callback help.
"""
users_app = typer.Typer(callback=old_callback, name="users")
def new_users():
"""
I have the highland! Create some users.
"""
app.add_typer(users_app, callback=new_users, name="new-users")
@users_app.callback()
def users():
"""
Manage users in the app.
"""
@users_app.command()
def create(name: str):
print(f"Creating user: {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/subcommands/name_help/tutorial006_py310.py
================================================
import typer
app = typer.Typer()
def old_callback():
"""
Old callback help.
"""
users_app = typer.Typer(callback=old_callback, name="exp-users", help="Explicit help.")
def new_users():
"""
I have the highland! Create some users.
"""
app.add_typer(users_app, callback=new_users)
@users_app.callback()
def users():
"""
Manage users in the app.
"""
@users_app.command()
def create(name: str):
print(f"Creating user: {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/subcommands/name_help/tutorial007_py310.py
================================================
import typer
app = typer.Typer()
def old_callback():
"""
Old callback help.
"""
users_app = typer.Typer(callback=old_callback, name="users", help="Explicit help.")
def new_users():
"""
I have the highland! Create some users.
"""
app.add_typer(users_app, callback=new_users)
@users_app.callback(help="Help from callback for users.")
def users():
"""
Manage users in the app.
"""
@users_app.command()
def create(name: str):
print(f"Creating user: {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/subcommands/name_help/tutorial008_py310.py
================================================
import typer
app = typer.Typer()
def old_callback():
"""
Old callback help.
"""
users_app = typer.Typer(callback=old_callback, name="exp-users", help="Explicit help.")
def new_users():
"""
I have the highland! Create some users.
"""
app.add_typer(
users_app,
callback=new_users,
name="cake-sith-users",
help="Unlimited powder! Eh, users.",
)
@users_app.callback(help="Help from callback for users.")
def users():
"""
Manage users in the app.
"""
@users_app.command()
def create(name: str):
print(f"Creating user: {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/subcommands/tutorial001_py310/__init__.py
================================================
================================================
FILE: docs_src/subcommands/tutorial001_py310/items.py
================================================
import typer
app = typer.Typer()
@app.command()
def create(item: str):
print(f"Creating item: {item}")
@app.command()
def delete(item: str):
print(f"Deleting item: {item}")
@app.command()
def sell(item: str):
print(f"Selling item: {item}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/subcommands/tutorial001_py310/main.py
================================================
import typer
import items
import users
app = typer.Typer()
app.add_typer(users.app, name="users")
app.add_typer(items.app, name="items")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/subcommands/tutorial001_py310/users.py
================================================
import typer
app = typer.Typer()
@app.command()
def create(user_name: str):
print(f"Creating user: {user_name}")
@app.command()
def delete(user_name: str):
print(f"Deleting user: {user_name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/subcommands/tutorial002_py310/__init__.py
================================================
================================================
FILE: docs_src/subcommands/tutorial002_py310/main.py
================================================
import typer
app = typer.Typer()
items_app = typer.Typer()
app.add_typer(items_app, name="items")
users_app = typer.Typer()
app.add_typer(users_app, name="users")
@items_app.command("create")
def items_create(item: str):
print(f"Creating item: {item}")
@items_app.command("delete")
def items_delete(item: str):
print(f"Deleting item: {item}")
@items_app.command("sell")
def items_sell(item: str):
print(f"Selling item: {item}")
@users_app.command("create")
def users_create(user_name: str):
print(f"Creating user: {user_name}")
@users_app.command("delete")
def users_delete(user_name: str):
print(f"Deleting user: {user_name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/subcommands/tutorial003_py310/__init__.py
================================================
================================================
FILE: docs_src/subcommands/tutorial003_py310/items.py
================================================
import typer
app = typer.Typer()
@app.command()
def create(item: str):
print(f"Creating item: {item}")
@app.command()
def delete(item: str):
print(f"Deleting item: {item}")
@app.command()
def sell(item: str):
print(f"Selling item: {item}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/subcommands/tutorial003_py310/lands.py
================================================
import typer
import reigns
import towns
app = typer.Typer()
app.add_typer(reigns.app, name="reigns")
app.add_typer(towns.app, name="towns")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/subcommands/tutorial003_py310/main.py
================================================
import typer
import items
import lands
import users
app = typer.Typer()
app.add_typer(users.app, name="users")
app.add_typer(items.app, name="items")
app.add_typer(lands.app, name="lands")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/subcommands/tutorial003_py310/reigns.py
================================================
import typer
app = typer.Typer()
@app.command()
def conquer(name: str):
print(f"Conquering reign: {name}")
@app.command()
def destroy(name: str):
print(f"Destroying reign: {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/subcommands/tutorial003_py310/towns.py
================================================
import typer
app = typer.Typer()
@app.command()
def found(name: str):
print(f"Founding town: {name}")
@app.command()
def burn(name: str):
print(f"Burning town: {name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/subcommands/tutorial003_py310/users.py
================================================
import typer
app = typer.Typer()
@app.command()
def create(user_name: str):
print(f"Creating user: {user_name}")
@app.command()
def delete(user_name: str):
print(f"Deleting user: {user_name}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/terminating/__init__.py
================================================
================================================
FILE: docs_src/terminating/tutorial001_py310.py
================================================
import typer
existing_usernames = ["rick", "morty"]
def maybe_create_user(username: str):
if username in existing_usernames:
print("The user already exists")
raise typer.Exit()
else:
print(f"User created: {username}")
def send_new_user_notification(username: str):
# Somehow send a notification here for the new user, maybe an email
print(f"Notification sent for new user: {username}")
app = typer.Typer()
@app.command()
def main(username: str):
maybe_create_user(username=username)
send_new_user_notification(username=username)
if __name__ == "__main__":
app()
================================================
FILE: docs_src/terminating/tutorial002_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(username: str):
if username == "root":
print("The root user is reserved")
raise typer.Exit(code=1)
print(f"New user created: {username}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/terminating/tutorial003_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(username: str):
if username == "root":
print("The root user is reserved")
raise typer.Abort()
print(f"New user created: {username}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/testing/__init__.py
================================================
================================================
FILE: docs_src/testing/app01_py310/__init__.py
================================================
================================================
FILE: docs_src/testing/app01_py310/main.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(name: str, city: str | None = None):
print(f"Hello {name}")
if city:
print(f"Let's have a coffee in {city}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/testing/app01_py310/test_main.py
================================================
from typer.testing import CliRunner
from .main import app
runner = CliRunner()
def test_app():
result = runner.invoke(app, ["Camila", "--city", "Berlin"])
assert result.exit_code == 0
assert "Hello Camila" in result.output
assert "Let's have a coffee in Berlin" in result.output
================================================
FILE: docs_src/testing/app02_an_py310/__init__.py
================================================
================================================
FILE: docs_src/testing/app02_an_py310/main.py
================================================
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(name: str, email: Annotated[str, typer.Option(prompt=True)]):
print(f"Hello {name}, your email is: {email}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/testing/app02_an_py310/test_main.py
================================================
from typer.testing import CliRunner
from .main import app
runner = CliRunner()
def test_app():
result = runner.invoke(app, ["Camila"], input="camila@example.com\n")
assert result.exit_code == 0
assert "Hello Camila, your email is: camila@example.com" in result.output
================================================
FILE: docs_src/testing/app02_py310/__init__.py
================================================
================================================
FILE: docs_src/testing/app02_py310/main.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(name: str, email: str = typer.Option(..., prompt=True)):
print(f"Hello {name}, your email is: {email}")
if __name__ == "__main__":
app()
================================================
FILE: docs_src/testing/app02_py310/test_main.py
================================================
from typer.testing import CliRunner
from .main import app
runner = CliRunner()
def test_app():
result = runner.invoke(app, ["Camila"], input="camila@example.com\n")
assert result.exit_code == 0
assert "Hello Camila, your email is: camila@example.com" in result.output
================================================
FILE: docs_src/testing/app03_py310/__init__.py
================================================
================================================
FILE: docs_src/testing/app03_py310/main.py
================================================
import typer
def main(name: str = "World"):
print(f"Hello {name}")
if __name__ == "__main__":
typer.run(main)
================================================
FILE: docs_src/testing/app03_py310/test_main.py
================================================
import typer
from typer.testing import CliRunner
from .main import main
app = typer.Typer()
app.command()(main)
runner = CliRunner()
def test_app():
result = runner.invoke(app, ["--name", "Camila"])
assert result.exit_code == 0
assert "Hello Camila" in result.output
================================================
FILE: docs_src/typer_app/__init__.py
================================================
================================================
FILE: docs_src/typer_app/tutorial001_py310.py
================================================
import typer
app = typer.Typer()
@app.command()
def main(name: str):
print(f"Hello {name}")
if __name__ == "__main__":
app()
================================================
FILE: mkdocs.env.yml
================================================
# Define this here and not in the main mkdocs.yml file because that one could be auto
# updated and written, and the script would remove the env var
markdown_extensions:
pymdownx.highlight:
linenums: !ENV [LINENUMS, false]
================================================
FILE: mkdocs.yml
================================================
INHERIT: ./mkdocs.env.yml
site_name: Typer
site_description: Typer, build great CLIs. Easy to code. Based on Python type hints.
site_url: https://typer.tiangolo.com/
theme:
name: material
custom_dir: docs/overrides
palette:
- media: "(prefers-color-scheme)"
toggle:
icon: material/lightbulb-auto
name: Switch to light mode
- media: '(prefers-color-scheme: light)'
scheme: default
primary: black
accent: teal
toggle:
icon: material/lightbulb
name: Switch to dark mode
- media: '(prefers-color-scheme: dark)'
scheme: slate
primary: black
accent: teal
toggle:
icon: material/lightbulb-outline
name: Switch to system preference
features:
- content.code.annotate
- content.code.copy
# - content.code.select
- content.footnote.tooltips
- content.tabs.link
- content.tooltips
- navigation.footer
- navigation.indexes
- navigation.instant
- navigation.instant.prefetch
# - navigation.instant.preview
- navigation.instant.progress
- navigation.path
- navigation.tabs
- navigation.tabs.sticky
- navigation.top
- navigation.tracking
- search.highlight
- search.share
- search.suggest
- toc.follow
icon:
repo: fontawesome/brands/github-alt
logo: img/icon.svg
favicon: img/favicon.png
language: en
repo_name: fastapi/typer
repo_url: https://github.com/fastapi/typer
plugins:
# Material for MkDocs
search:
social:
typeset:
# Other plugins
macros:
include_yaml:
- members: data/members.yml
redirects:
redirect_maps:
typer-cli.md: tutorial/typer-command.md
mkdocstrings:
handlers:
python:
options:
extensions:
- griffe_typingdoc
show_root_heading: true
show_if_no_docstring: true
inherited_members: true
members_order: source
separate_signature: true
unwrap_annotated: true
filters:
- '!^_'
merge_init_into_class: true
docstring_section_style: spacy
signature_crossrefs: true
show_symbol_type_heading: true
show_symbol_type_toc: true
nav:
- Typer: index.md
- features.md
- Tutorial - User Guide:
- tutorial/index.md
- environment-variables.md
- virtual-environments.md
- tutorial/install.md
- tutorial/first-steps.md
- tutorial/typer-app.md
- tutorial/printing.md
- tutorial/terminating.md
- CLI Arguments:
- tutorial/arguments/index.md
- tutorial/arguments/optional.md
- tutorial/arguments/default.md
- tutorial/arguments/help.md
- tutorial/arguments/envvar.md
- tutorial/arguments/other-uses.md
- CLI Options:
- tutorial/options/index.md
- tutorial/options/help.md
- tutorial/options/required.md
- tutorial/options/prompt.md
- tutorial/options/password.md
- tutorial/options/name.md
- tutorial/options/callback-and-context.md
- tutorial/options/version.md
- Commands:
- tutorial/commands/index.md
- tutorial/commands/arguments.md
- tutorial/commands/options.md
- tutorial/commands/help.md
- tutorial/commands/name.md
- tutorial/commands/callback.md
- tutorial/commands/one-or-multiple.md
- tutorial/commands/context.md
- tutorial/options-autocompletion.md
- CLI Parameter Types:
- tutorial/parameter-types/index.md
- tutorial/parameter-types/number.md
- tutorial/parameter-types/bool.md
- tutorial/parameter-types/uuid.md
- tutorial/parameter-types/datetime.md
- tutorial/parameter-types/enum.md
- tutorial/parameter-types/path.md
- tutorial/parameter-types/file.md
- tutorial/parameter-types/custom-types.md
- SubCommands - Command Groups:
- tutorial/subcommands/index.md
- tutorial/subcommands/add-typer.md
- tutorial/subcommands/single-file.md
- tutorial/subcommands/nested-subcommands.md
- tutorial/subcommands/callback-override.md
- tutorial/subcommands/name-and-help.md
- Multiple Values:
- tutorial/multiple-values/index.md
- tutorial/multiple-values/multiple-options.md
- tutorial/multiple-values/options-with-multiple-values.md
- tutorial/multiple-values/arguments-with-multiple-values.md
- tutorial/prompt.md
- tutorial/progressbar.md
- tutorial/app-dir.md
- tutorial/launch.md
- tutorial/testing.md
- tutorial/package.md
- tutorial/exceptions.md
- tutorial/one-file-per-command.md
- tutorial/typer-command.md
- Reference (Code API):
- reference/index.md
- reference/typer.md
- reference/run_launch.md
- reference/parameters.md
- reference/file_objects.md
- reference/context.md
- Resources:
- resources/index.md
- help-typer.md
- contributing.md
- management-tasks.md
- About:
- about/index.md
- alternatives.md
- management.md
- release-notes.md
markdown_extensions:
# Material for MkDocs Extensions
material.extensions.preview:
targets:
include:
- "*"
# Python Markdown
abbr:
attr_list:
footnotes:
md_in_html:
tables:
toc:
permalink: true
# Python Markdown Extensions
pymdownx.betterem:
smart_enable: all
pymdownx.caret:
pymdownx.highlight:
line_spans: __span
pymdownx.inlinehilite:
pymdownx.keys:
pymdownx.mark:
pymdownx.superfences:
custom_fences:
- name: mermaid
class: mermaid
format: !!python/name:pymdownx.superfences.fence_code_format
pymdownx.tilde:
# pymdownx blocks
pymdownx.blocks.admonition:
types:
- note
- attention
- caution
- danger
- error
- tip
- hint
- warning
# Custom types
- info
- check
pymdownx.blocks.details:
pymdownx.blocks.tab:
alternate_style: True
# Other extensions
mdx_include:
markdown_include_variants:
extra:
social:
- icon: fontawesome/brands/github-alt
link: https://github.com/fastapi/typer
- icon: fontawesome/brands/twitter
link: https://twitter.com/tiangolo
- icon: fontawesome/brands/linkedin
link: https://www.linkedin.com/in/tiangolo
- icon: fontawesome/brands/dev
link: https://dev.to/tiangolo
- icon: fontawesome/brands/medium
link: https://medium.com/@tiangolo
- icon: fontawesome/solid/globe
link: https://tiangolo.com
extra_css:
- css/termynal.css
- css/custom.css
extra_javascript:
- js/termynal.js
- js/custom.js
hooks:
- scripts/mkdocs_hooks.py
================================================
FILE: pyproject.toml
================================================
[build-system]
requires = ["pdm-backend"]
build-backend = "pdm.backend"
[project]
name = "typer"
license = "MIT"
license-files = ["LICENSE"]
dynamic = ["version"]
description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
authors = [
{name = "Sebastián Ramírez", email = "tiangolo@gmail.com"},
]
requires-python = ">=3.10"
classifiers = [
"Intended Audience :: Information Technology",
"Intended Audience :: System Administrators",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python",
"Topic :: Software Development :: Libraries :: Application Frameworks",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Software Development :: Libraries",
"Topic :: Software Development",
"Typing :: Typed",
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
]
dependencies = [
"click >= 8.2.1",
"shellingham >=1.3.0",
"rich >=12.3.0",
"annotated-doc >=0.0.2",
]
readme = "README.md"
[project.urls]
Homepage = "https://github.com/fastapi/typer"
Documentation = "https://typer.tiangolo.com"
Repository = "https://github.com/fastapi/typer"
Issues = "https://github.com/fastapi/typer/issues"
Changelog = "https://typer.tiangolo.com/release-notes/"
[project.scripts]
typer = "typer.cli:main"
[dependency-groups]
dev = [
{ include-group = "tests" },
{ include-group = "docs" },
"prek >=0.3.2",
]
docs = [
"cairosvg >=2.8.2",
"griffe-typingdoc >=0.3.0",
"griffe-warnings-deprecated >=1.1.0",
"markdown-include-variants >=0.0.8",
"mdx-include >=1.4.1",
"mkdocs-macros-plugin >=1.5.0",
"mkdocs-material >=9.7.1",
"mkdocs-redirects >=1.2.1",
"mkdocstrings[python] >=0.30.1",
"pillow >=11.3.0",
"pyyaml >=5.3.1",
]
github-actions = [
"httpx >=0.27.0",
"pydantic >=2.5.3",
"pydantic-settings >=2.1.0",
"pygithub >=2.3.0",
"smokeshow >=0.5.0",
]
tests = [
"coverage[toml] >=7.13",
"mypy >=1.19.1",
"ty >=0.0.9",
"pytest >=9.0.0",
"pytest-cov >=4.0.0",
"pytest-sugar >=0.9.5",
"pytest-xdist >=1.32.0",
"rich >=12.3.0",
"ruff >=0.15.0",
"shellingham >=1.3.0",
]
[tool.pdm]
version = { source = "file", path = "typer/__init__.py" }
distribution = true
[tool.pdm.build]
source-includes = [
"tests/",
"docs_src/",
"scripts/",
]
[tool.pytest]
minversion = "9.0"
addopts = [
"--strict-config",
"--strict-markers",
]
strict_xfail = true
filterwarnings = [
"error",
# For pytest-xdist
'ignore::DeprecationWarning:xdist',
]
[tool.coverage.run]
parallel = true
data_file = "coverage/.coverage"
source = [
"docs_src",
"tests",
"typer"
]
omit = [
"typer/_typing.py",
]
context = '${CONTEXT}'
relative_files = true
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"@overload",
'if __name__ == "__main__":',
"if TYPE_CHECKING:",
]
[tool.mypy]
strict = true
[[tool.mypy.overrides]]
module = "docs_src.*"
disallow_incomplete_defs = false
disallow_untyped_defs = false
disallow_untyped_calls = false
[[tool.mypy.overrides]]
module = "shellingham"
ignore_missing_imports = true
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
"TID", # flake8-tidy-imports
]
ignore = [
"E501", # line too long, handled by black
"B008", # do not perform function calls in argument defaults
"C901", # too complex
"W191", # indentation contains tabs
"TID252", # relative imports okay
]
[tool.ruff.lint.per-file-ignores]
# "__init__.py" = ["F401"]
# rich_utils is allowed to use rich imports
"typer/rich_utils.py" = ["TID251"]
# This file is more readable without yield from
"docs_src/progressbar/tutorial004_py310.py" = ["UP028", "B007"]
# Default mutable data structure
"docs_src/options_autocompletion/tutorial006_an_py310.py" = ["B006"]
"docs_src/multiple_values/multiple_options/tutorial002_an_py310.py" = ["B006"]
"docs_src/options_autocompletion/tutorial007_an_py310.py" = ["B006"]
"docs_src/options_autocompletion/tutorial008_an_py310.py" = ["B006"]
"docs_src/options_autocompletion/tutorial009_an_py310.py" = ["B006"]
"docs_src/parameter_types/enum/tutorial003_an_py310.py" = ["B006"]
# Loop control variable `value` not used within loop body
"docs_src/progressbar/tutorial001_py310.py" = ["B007"]
"docs_src/progressbar/tutorial003_py310.py" = ["B007"]
"docs_src/progressbar/tutorial005_py310.py" = ["B007"]
"docs_src/progressbar/tutorial006_py310.py" = ["B007"]
# Local variable `delete` is assigned to but never used
"docs_src/prompt/tutorial003_py310.py" = ["F841"]
# No need to worry about rich imports in docs
"docs_src/*" = ["TID"]
[tool.ruff.lint.isort]
known-third-party = ["typer", "click"]
# For docs_src/subcommands/tutorial003/
known-first-party = ["reigns", "towns", "lands", "items", "users"]
[tool.ruff.lint.pyupgrade]
# Preserve types, even if a file imports `from __future__ import annotations`.
keep-runtime-typing = true
[tool.ruff.lint.flake8-tidy-imports]
# Import rich_utils from within functions (lazy), not at the module level (TID253)
banned-module-level-imports = ["typer.rich_utils"]
[tool.ruff.lint.flake8-tidy-imports.banned-api]
"rich".msg = "Use 'typer.rich_utils' instead of importing from 'rich' directly."
"shellingham.detect_shell".msg = """\
Use 'typer._completion_shared._get_shell_name' instead of using \
'shellingham.detect_shell' directly.
"""
================================================
FILE: scripts/deploy_docs_status.py
================================================
import logging
import re
from typing import Literal
from github import Auth, Github
from pydantic import BaseModel, SecretStr
from pydantic_settings import BaseSettings
site_domain = "typer.tiangolo.com"
class Settings(BaseSettings):
github_repository: str
github_token: SecretStr
deploy_url: str | None = None
commit_sha: str
run_id: int
state: Literal["pending", "success", "error"] = "pending"
class LinkData(BaseModel):
previous_link: str
preview_link: str
def main() -> None:
logging.basicConfig(level=logging.INFO)
settings = Settings()
logging.info(f"Using config: {settings.model_dump_json()}")
g = Github(auth=Auth.Token(settings.github_token.get_secret_value()))
repo = g.get_repo(settings.github_repository)
use_pr = next(
(pr for pr in repo.get_pulls() if pr.head.sha == settings.commit_sha), None
)
if not use_pr:
logging.error(f"No PR found for hash: {settings.commit_sha}")
return
commits = list(use_pr.get_commits())
current_commit = [c for c in commits if c.sha == settings.commit_sha][0]
run_url = f"https://github.com/{settings.github_repository}/actions/runs/{settings.run_id}"
if settings.state == "pending":
current_commit.create_status(
state="pending",
description="Deploying Docs",
context="deploy-docs",
target_url=run_url,
)
logging.info("No deploy URL available yet")
return
if settings.state == "error":
current_commit.create_status(
state="error",
description="Error Deploying Docs",
context="deploy-docs",
target_url=run_url,
)
logging.info("Error deploying docs")
return
assert settings.state == "success"
if not settings.deploy_url:
current_commit.create_status(
state="success",
description="No Docs Changes",
context="deploy-docs",
target_url=run_url,
)
logging.info("No docs changes found")
return
assert settings.deploy_url
current_commit.create_status(
state="success",
description="Docs Deployed",
context="deploy-docs",
target_url=run_url,
)
files = list(use_pr.get_files())
docs_files = [f for f in files if f.filename.startswith("docs/")]
deploy_url = settings.deploy_url.rstrip("/")
links: list[LinkData] = []
for f in docs_files:
match = re.match(r"docs/(.*)", f.filename)
if not match:
continue
path = match.group(1)
if path.endswith("index.md"):
use_path = path.replace("index.md", "")
else:
use_path = path.replace(".md", "/")
link = LinkData(
previous_link=f"https://{site_domain}/{use_path}",
preview_link=f"{deploy_url}/{use_path}",
)
links.append(link)
links.sort(key=lambda x: x.preview_link)
header = "## 📝 Docs preview"
message = header
message += f"\n\nLast commit {settings.commit_sha} at: {deploy_url}"
if links:
message += "\n\n### Modified Pages\n\n"
for link in links:
message += f"* {link.preview_link}"
message += f" - ([before]({link.previous_link}))"
message += "\n"
print(message)
issue = use_pr.as_issue()
comments = list(issue.get_comments())
for comment in comments:
if (
comment.body.startswith(header)
and comment.user.login == "github-actions[bot]"
):
comment.edit(message)
break
else:
issue.create_comment(message)
logging.info("Finished")
if __name__ == "__main__":
main()
================================================
FILE: scripts/docker/Dockerfile
================================================
FROM python:latest
# Add Fish
RUN echo 'deb http://download.opensuse.org/repositories/shells:/fish:/release:/3/Debian_12/ /' | tee /etc/apt/sources.list.d/shells:fish:release:3.list
RUN curl -fsSL https://download.opensuse.org/repositories/shells:fish:release:3/Debian_12/Release.key | gpg --dearmor | tee /etc/apt/trusted.gpg.d/shells_fish_release_3.gpg > /dev/null
# Install packages including Fish, Zsh, PowerShell
RUN apt-get update && apt-get install -y \
wget \
apt-transport-https \
software-properties-common \
nano \
vim \
fish \
zsh \
&& wget https://github.com/PowerShell/PowerShell/releases/download/v7.4.4/powershell_7.4.4-1.deb_amd64.deb \
&& dpkg -i powershell_7.4.4-1.deb_amd64.deb
# Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
ENV UV_SYSTEM_PYTHON=1
COPY . /code
WORKDIR /code
RUN uv pip install -r requirements.txt
================================================
FILE: scripts/docker/compose.yaml
================================================
services:
typer:
build:
context: ../../
dockerfile: scripts/docker/Dockerfile
volumes:
- ../../:/code
command: sleep infinity
================================================
FILE: scripts/docs.py
================================================
import logging
import os
import re
import shutil
import subprocess
from http.server import HTTPServer, SimpleHTTPRequestHandler
from pathlib import Path
import typer
from ruff.__main__ import find_ruff_bin
logging.basicConfig(level=logging.INFO)
mkdocs_name = "mkdocs.yml"
docs_path = Path("docs")
en_docs_path = Path("")
app = typer.Typer()
@app.callback()
def callback() -> None:
# For MacOS with Cairo
os.environ["DYLD_FALLBACK_LIBRARY_PATH"] = "/opt/homebrew/lib"
def generate_readme_content() -> str:
en_index = en_docs_path / "docs" / "index.md"
content = en_index.read_text("utf-8")
match_pre = re.search(r"\n\n", content)
if not match_pre:
raise RuntimeError("Couldn't find pre section (