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:
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.:
content = "la cigüeña trae al niño"
instead of having bytes
, e.g.:
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:`
import typer
from typing_extensions import Annotated
def main(config: Annotated[typer.FileText, typer.Option()]):
for line in config:
print(f"Config line: {line}")
if __name__ == "__main__":
typer.run(main)
Tip
Prefer to use the Annotated
version if possible.
import typer
def main(config: typer.FileText = typer.Option(...)):
for line in config:
print(f"Config line: {line}")
if __name__ == "__main__":
typer.run(main)
Check it:
// 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
:
import typer
from typing_extensions import Annotated
def main(config: Annotated[typer.FileTextWrite, typer.Option()]):
config.write("Some config written by the app")
print("Config written")
if __name__ == "__main__":
typer.run(main)
Tip
Prefer to use the Annotated
version if possible.
import typer
def main(config: typer.FileTextWrite = typer.Option(...)):
config.write("Some config written by the app")
print("Config written")
if __name__ == "__main__":
typer.run(main)
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:
$ python main.py --config text.txt
Config written
// Check the contents of the file
$ cat text.txt
Some config written by the app
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:
import typer
from typing_extensions import Annotated
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__":
typer.run(main)
Tip
Prefer to use the Annotated
version if possible.
import typer
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__":
typer.run(main)
Check it:
$ 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
.
import typer
from typing_extensions import Annotated
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__":
typer.run(main)
Tip
Prefer to use the Annotated
version if possible.
import typer
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__":
typer.run(main)
$ 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, Click 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'slazy=True
for writing andlazy=False
for reading.
- By default, when writing files, Click 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
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:
import typer
from typing_extensions import Annotated
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__":
typer.run(main)
Tip
Prefer to use the Annotated
version if possible.
import typer
def main(config: typer.FileText = typer.Option(..., mode="a")):
config.write("This is a single line\n")
print("Config line written")
if __name__ == "__main__":
typer.run(main)
Tip
As you are manually setting mode="a"
, you can use typer.FileText
or typer.FileTextWrite
, both will work.
Check it:
$ 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
About the different types¶
Info
These are technical details about why the different types/classes provided by Typer.
But you don't need this information to be able to use them. You can skip it.
Typer provides you these different types (classes) because they inherit directly from the actual Python implementation that will be provided underneath for each case.
This way your editor will give you the right type checks and completion for each type.
Even if you use lazy
. When you use lazy
Click creates a especial object to delay writes, and serves as a "proxy" to the actual file that will be written. But this especial proxy object doesn't expose the attributes and methods needed for type checks and completion in the editor. If you access those attributes or call the methods, the "proxy" lazy object will call them in the final object and it will all work. But you wouldn't get autocompletion for them.
But because these Typer classes inherit from the actual implementation that will be provided underneath (not the lazy object), you will get all the autocompletion and type checks in the editor.