76

I am currently using argparse like this:

import argparse
from argparse import ArgumentParser

parser = ArgumentParser(description="ikjMatrix multiplication")
parser.add_argument("-i", dest="filename", required=True,
    help="input file with two matrices", metavar="FILE")
args = parser.parse_args()

A, B = read(args.filename)
C = ikjMatrixProduct(A, B)
printMatrix(C)

Now I would like to note, that the argument of -i should be a readable file. How can I do that?

I've tried adding type=open, type=argparse.FileType('r') and they worked, but if the file is not valid, I would like to get an error message. How can I do that?

Martin Thoma
  • 124,992
  • 159
  • 614
  • 958

4 Answers4

106

It's pretty easy actually. You just need to write a function which checks if the file is valid and writes an error otherwise. Use that function with the type option. Note that you could get more fancy and create a custom action by subclassing argparse.Action, but I don't think that is necessary here. In my example, I return an open file handle (see below):

#!/usr/bin/env python

from argparse import ArgumentParser
import os.path


def is_valid_file(parser, arg):
    if not os.path.exists(arg):
        parser.error("The file %s does not exist!" % arg)
    else:
        return open(arg, 'r')  # return an open file handle


parser = ArgumentParser(description="ikjMatrix multiplication")
parser.add_argument("-i", dest="filename", required=True,
                    help="input file with two matrices", metavar="FILE",
                    type=lambda x: is_valid_file(parser, x))
args = parser.parse_args()

A, B = read(args.filename)
C = ikjMatrixProduct(A, B)
printMatrix(C)
Martin Thoma
  • 124,992
  • 159
  • 614
  • 958
mgilson
  • 300,191
  • 65
  • 633
  • 696
  • 3
    @moose -- One other comment. `os.path.isfile` might be more appropriate than `os.path.exists` (depending on whether you want to accept directories too) – mgilson Jul 18 '12 at 12:44
  • 16
    Actually it is considered better practice to try and open the file with a try-except block, than to check for existence – jarondl Jul 06 '13 at 12:45
  • 5
    @jarondl is right. This should be changed to use a `try: ... except IOError` to avoid potential race conditions. For most cases it won't matter but this has bitten me recently. – AlexLordThorsen May 27 '15 at 00:27
  • Raising `argparse.ArgumentTypeError` makes code more concise.(as noted in moose's answer) – industryworker3595112 Nov 29 '16 at 06:41
  • 1
    You dont have to `try` opening the file because `open` will handle all of the error cases by itself. Then you might catch that in the `except` clause, but I think you don't need it for that case. – Konstantin Sekeresh Jan 28 '19 at 08:57
64

A way to do this in Python 3.4 is to use the argparse.FileType class. Make sure to close the input stream when you are finished. This is also useful because you can use the pseudo-argument '-' for STDIN/STDOUT. From the documentation:

FileType objects understand the pseudo-argument '-' and automatically convert this into sys.stdin for readable FileType objects and sys.stdout for writable FileType objects

Example:

#!/usr/bin/env python3

import argparse

if __name__ == '__main__':
  parser = argparse.ArgumentParser()
  parser.add_argument('--infile', type=argparse.FileType('r', encoding='UTF-8'), 
                      required=True)
  args = parser.parse_args()
  print(args)
  args.infile.close()

And then when ran...

  • Without argument:

    $ ./stack_overflow.py
    usage: stack_overflow.py [-h] --infile INFILE
    stack_overflow.py: error: the following arguments are required: --infile
    
  • With nonexistent file:

    $ ./stack_overflow.py --infile notme
    usage: stack_overflow.py [-h] --infile INFILE
    stack_overflow.py: error: argument --infile: can't open 'notme': [Errno 2] No such file or directory: 'notme'
    
  • With an existing file:

    $ ./stack_overflow.py --infile ./stack_overflow.py
    Namespace(infile=<_io.TextIOWrapper name='./stack_overflow.py' mode='r' encoding='UTF-8'>)
    
  • Using '-' for STDIN:

    $ echo 'hello' | ./stack_overflow.py --infile -
    Namespace(infile=<_io.TextIOWrapper name='<stdin>' mode='r' encoding='UTF-8'>)
    
mkobit
  • 43,979
  • 12
  • 156
  • 150
  • 8
    `argparse.FileType` is also available in Python 2.7. – jared Jul 11 '16 at 18:22
  • What happens if there are multiple usages of `argparse.FileType` for different input parameters, and they are all using `-`? – CMCDragonkai Apr 17 '18 at 01:57
  • @CMCDragonkai, from the docs: "If sys.stdin is used more than once, the second and further use will return no lines, except perhaps for interactive use, or if it has been explicitly reset (e.g. using sys.stdin.seek(0))." – shx2 May 25 '20 at 05:53
21

I have just found this one:

def extant_file(x):
    """
    'Type' for argparse - checks that file exists but does not open.
    """
    if not os.path.exists(x):
        # Argparse uses the ArgumentTypeError to give a rejection message like:
        # error: argument input: x does not exist
        raise argparse.ArgumentTypeError("{0} does not exist".format(x))
    return x

if __name__ == "__main__":
    import argparse, sys, os
    from argparse import ArgumentParser

    parser = ArgumentParser(description="ikjMatrix multiplication")
    parser.add_argument("-i", "--input",
        dest="filename", required=True, type=extant_file,
        help="input file with two matrices", metavar="FILE")
    args = parser.parse_args()

    A, B = read(args.filename)
    C = ikjMatrixProduct(A, B)
    printMatrix(C, args.output)

Source: fhcrc.github.com

Kenny Dewhirst
  • 73
  • 3
  • 11
Martin Thoma
  • 124,992
  • 159
  • 614
  • 958
4

Using argparse on python3.8+. This returns Pathlib.Path.

import argparse
from pathlib import Path

def validate_file(arg):
    if (file := Path(arg)).is_file():
        return file
    else:
        raise FileNotFoundError(arg)

parser = argparse.ArgumentParser()
parser.add_argument(
    "--file", type=validate_file, help="Input file path", required=True
)
args = parser.parse_args()
Amir
  • 2,082
  • 4
  • 22
  • 28