Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions docs/tutorial/parameter-types/date.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
You can specify a *CLI parameter* as a Python <a href="https://docs.python.org/3/library/datetime.html" class="external-link" target="_blank">`date`</a>.

Your function will receive a standard Python `date` object, and again, your editor will give you completion, etc.

```Python hl_lines="2 7 8 9"
{!../docs_src/parameter_types/date/tutorial001.py!}
```
Copy link
Member

@svlandeg svlandeg Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a quick note here that if we do want to progress with this PR at some point, the docs should be updated with the new format, and using Typer() explicitely in the tutorial examples.


Typer will accept any string from the following formats:

* `%Y-%m-%d`

Check it:

<div class="termy">

```console
$ python main.py --help

Usage: main.py [OPTIONS] BIRTH:[%Y-%m-%d]

Arguments:
BIRTH:[%Y-%m-%d][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.

// Pass a date
$ python main.py 1956-01-31

Interesting day to be born: 1956-01-31
Birth week name: Tuesday

// An invalid date
$ python main.py july-19-1989

Usage: main.py [OPTIONS] BIRTH:[%Y-%m-%d]

Error: Invalid value for 'BIRTH:[%Y-%m-%d]': 'july-19-1989' does not match the format '%Y-%m-%d'.
```

</div>

## Custom date format

You can also customize the formats received for the `date` with the `formats` parameter.

`formats` receives a list of strings with the date formats that would be passed to <a href="https://docs.python.org/3/library/datetime.html#datetime.date.strftime" class="external-link" target="_blank">datetime.strptime()</a>.

For example, let's imagine that you want to accept an ISO formatted date, 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:

```Python hl_lines="8"
{!../docs_src/parameter_types/date/tutorial002.py!}
```

!!! tip
Notice the last string in `formats`: `"%m/%d/%Y"`.

Check it:

<div class="termy">

```console
// ISO dates work
$ python main.py 1969-10-29

Launch will be at: 1969-10-29

// But the strange custom format also works
$ python main.py 10/29/1969

Launch will be at: 1969-10-29
```

</div>
Empty file.
13 changes: 13 additions & 0 deletions docs_src/parameter_types/date/tutorial001.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from calendar import day_name
from datetime import date

import typer


def main(birth: date):
typer.echo(f"Interesting day to be born: {birth}")
typer.echo(f"Birth day name: {day_name[birth.weekday()]}")


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

import typer


def main(launch_date: date = typer.Argument(..., formats=["%Y-%m-%d", "%m/%d/%Y"])):
typer.echo(f"Launch will be at: {launch_date}")


if __name__ == "__main__":
typer.run(main)
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ nav:
- Boolean CLI Options: 'tutorial/parameter-types/bool.md'
- UUID: 'tutorial/parameter-types/uuid.md'
- DateTime: 'tutorial/parameter-types/datetime.md'
- Date: 'tutorial/parameter-types/date.md'
- Enum - Choices: 'tutorial/parameter-types/enum.md'
- Path: 'tutorial/parameter-types/path.md'
- File: 'tutorial/parameter-types/file.md'
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import subprocess

import typer
from typer.testing import CliRunner

from docs_src.parameter_types.date import tutorial001 as mod

runner = CliRunner()

app = typer.Typer()
app.command()(mod.main)


def test_help():
result = runner.invoke(app, ["--help"])
assert result.exit_code == 0
assert "[%Y-%m-%d]" in result.output


def test_main():
result = runner.invoke(app, ["1956-01-31"])
assert result.exit_code == 0
assert "Interesting day to be born: 1956-01-31" in result.output
assert "Birth day name: Tuesday" in result.output


def test_invalid():
result = runner.invoke(app, ["july-19-1989"])
assert result.exit_code != 0
# TODO: when deprecating Click 7, remove second option
assert (
"Error: Invalid value for 'BIRTH:[%Y-%m-%d]': 'july-19-1989' does not match the format '%Y-%m-%d'"
in result.output
or "Error: Invalid value for 'BIRTH:[%Y-%m-%d]': invalid datetime format: july-19-1989. (choose from %Y-%m-%d)"
in result.output
)


def test_script():
result = subprocess.run(
["coverage", "run", mod.__file__, "--help"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding="utf-8",
)
assert "Usage" in result.stdout
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import subprocess

import typer
from typer.testing import CliRunner

from docs_src.parameter_types.date import tutorial002 as mod

runner = CliRunner()

app = typer.Typer()
app.command()(mod.main)


def test_main():
result = runner.invoke(app, ["1969-10-29"])
assert result.exit_code == 0
assert "Launch will be at: 1969-10-29" in result.output


def test_usa_weird_date_format():
result = runner.invoke(app, ["10/29/1969"])
assert result.exit_code == 0
assert "Launch will be at: 1969-10-29" in result.output


def test_script():
result = subprocess.run(
["coverage", "run", mod.__file__, "--help"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding="utf-8",
)
assert "Usage" in result.stdout
20 changes: 20 additions & 0 deletions typer/extra_click_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from datetime import date, datetime
from typing import Any, Optional, Sequence

import click


class Date(click.DateTime):
name = "date"

def __init__(self, formats: Optional[Sequence[str]] = None):
self.formats = formats or ["%Y-%m-%d"]

def _try_to_convert_date(self, value: Any, format: str) -> Optional[date]:
try:
return datetime.strptime(value, format).date()
except ValueError:
return None

def __repr__(self) -> str:
return "Date"
5 changes: 4 additions & 1 deletion typer/main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import inspect
from datetime import datetime
from datetime import date, datetime
from enum import Enum
from functools import update_wrapper
from pathlib import Path
Expand All @@ -8,6 +8,7 @@

import click

from . import extra_click_types
from .completion import get_completion_inspect_parameters
from .core import TyperArgument, TyperCommand, TyperGroup, TyperOption
from .models import (
Expand Down Expand Up @@ -534,6 +535,8 @@ def get_click_type(
return click.UUID
elif annotation == datetime:
return click.DateTime(formats=parameter_info.formats)
elif annotation == date:
return extra_click_types.Date(formats=parameter_info.formats)
elif (
annotation == Path
or parameter_info.allow_dash
Expand Down
12 changes: 6 additions & 6 deletions typer/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ def __init__(
min: Optional[Union[int, float]] = None,
max: Optional[Union[int, float]] = None,
clamp: bool = False,
# DateTime
# Date & DateTime
formats: Optional[Union[List[str]]] = None,
# File
mode: Optional[str] = None,
Expand Down Expand Up @@ -224,7 +224,7 @@ def __init__(
self.min = min
self.max = max
self.clamp = clamp
# DateTime
# Date & DateTime
self.formats = formats
# File
self.mode = mode
Expand Down Expand Up @@ -282,7 +282,7 @@ def __init__(
min: Optional[Union[int, float]] = None,
max: Optional[Union[int, float]] = None,
clamp: bool = False,
# DateTime
# Date & DateTime
formats: Optional[Union[List[str]]] = None,
# File
mode: Optional[str] = None,
Expand Down Expand Up @@ -322,7 +322,7 @@ def __init__(
min=min,
max=max,
clamp=clamp,
# DateTime
# Date & DateTime
formats=formats,
# File
mode=mode,
Expand Down Expand Up @@ -381,7 +381,7 @@ def __init__(
min: Optional[Union[int, float]] = None,
max: Optional[Union[int, float]] = None,
clamp: bool = False,
# DateTime
# Date & DateTime
formats: Optional[Union[List[str]]] = None,
# File
mode: Optional[str] = None,
Expand Down Expand Up @@ -421,7 +421,7 @@ def __init__(
min=min,
max=max,
clamp=clamp,
# DateTime
# Date & DateTime
formats=formats,
# File
mode=mode,
Expand Down
8 changes: 4 additions & 4 deletions typer/params.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def Option(
min: Optional[Union[int, float]] = None,
max: Optional[Union[int, float]] = None,
clamp: bool = False,
# DateTime
# Date & DateTime
formats: Optional[Union[List[str]]] = None,
# File
mode: Optional[str] = None,
Expand Down Expand Up @@ -93,7 +93,7 @@ def Option(
min=min,
max=max,
clamp=clamp,
# DateTime
# Date & DateTime
formats=formats,
# File
mode=mode,
Expand Down Expand Up @@ -141,7 +141,7 @@ def Argument(
min: Optional[Union[int, float]] = None,
max: Optional[Union[int, float]] = None,
clamp: bool = False,
# DateTime
# Date & DateTime
formats: Optional[Union[List[str]]] = None,
# File
mode: Optional[str] = None,
Expand Down Expand Up @@ -184,7 +184,7 @@ def Argument(
min=min,
max=max,
clamp=clamp,
# DateTime
# Date & DateTime
formats=formats,
# File
mode=mode,
Expand Down