100

I want to add a debug print statement test, if I enable --verbose from the command line and if I have the following in the script.

logger.info("test")

I went through the following questions, but couldn't get the answer...

Community
  • 1
  • 1
user1934146
  • 2,905
  • 10
  • 25
  • 25

7 Answers7

175

I find both --verbose (for users) and --debug (for developers) useful. Here's how I do it with logging and argparse:

import argparse
import logging

parser = argparse.ArgumentParser()
parser.add_argument(
    '-d', '--debug',
    help="Print lots of debugging statements",
    action="store_const", dest="loglevel", const=logging.DEBUG,
    default=logging.WARNING,
)
parser.add_argument(
    '-v', '--verbose',
    help="Be verbose",
    action="store_const", dest="loglevel", const=logging.INFO,
)
args = parser.parse_args()    
logging.basicConfig(level=args.loglevel)

So if --debug is set, the logging level is set to DEBUG. If --verbose, logging is set to INFO. If neither, the lack of --debug sets the logging level to the default of WARNING.

ypid
  • 1,708
  • 14
  • 10
Matthew Leingang
  • 2,579
  • 3
  • 20
  • 19
  • There's one catch: `logging` by default prints everything to stderr and you really want separate your normal program output (the one from `--verbose`) from dev-only (the one from `--debug`) – Michał Góral Jan 18 '16 at 22:43
  • 1
    @MichałGóral: That sounds like a good point. Can I use `logging.basicConfig` to send `info()` messages to stdout and `debug()` messages to stderr? – Matthew Leingang Jan 19 '16 at 17:56
  • 2
    @MatthewLeingang: To do that you'll have to use several [handlers](https://docs.python.org/2/howto/logging.html#handlers). Here's the example: http://stackoverflow.com/a/16066513/1088577 – Michał Góral Jan 19 '16 at 20:27
  • 2
    @MatthewLeingang When the output is a console/terminal it's usually better to send all messages to stderr. Sending them to stdout typically makes it more difficult to use your program in a unix pipeline. The rule of thumb for me is stdout -> program output, stderr -> program messages. – m000 Nov 21 '19 at 13:24
  • I don't really like this approach because it pollutes the arguments namespace with additional arguments. Also, the processing logic is kind of convoluted. If I had to use this approach, I'd put the default loglevel as a global, and would wrap the verbose/debug arguments in a mutual exclusion group (see: https://docs.python.org/3/library/argparse.html#mutual-exclusion). – m000 Nov 21 '19 at 13:29
  • @m000 (1): fair point, I think Michał's linked example addresses that. (2) I wasn't aware of the mutual exclusion group feature. If I were rewriting this answer now I would use it. – Matthew Leingang Nov 21 '19 at 18:39
  • 1
    This solution worked for me, except for some reason I had to use `logging.getLogger().setLevel(args.loglevel)` instead of `.basicConfig` – Dwigt Nov 04 '22 at 14:09
  • @Dwigt I wonder if the API has changed in the 9 years since I wrote this. I was probably using 3.2 then. – Matthew Leingang Nov 04 '22 at 18:39
  • @MatthewLeingang I'm sure it has :) I haven't confirmed this, but I read elsewhere that it could have something to do with the logger being declared already depending on how your code is setup, so calling `basicConfig` on it doesn't change anything and you need to reference the existing logger. Either way, great solution! – Dwigt Nov 08 '22 at 15:55
98

You need to combine the wisdom of the Argparse Tutorial with Python's Logging HOWTO. Here's an example...

> cat verbose.py 
#!/usr/bin/env python

import argparse
import logging

parser = argparse.ArgumentParser(
    description='A test script for http://stackoverflow.com/q/14097061/78845'
)
parser.add_argument("-v", "--verbose", help="increase output verbosity",
                    action="store_true")

args = parser.parse_args()
if args.verbose:
    logging.basicConfig(level=logging.DEBUG)

logging.debug('Only shown in debug mode')

Run the help:

> ./verbose.py -h
usage: verbose.py [-h] [-v]

A test script for http://stackoverflow.com/q/14097061/78845

optional arguments:
  -h, --help     show this help message and exit
  -v, --verbose  increase output verbosity

Running in verbose mode:

> ./verbose.py -v
DEBUG:root:Only shown in debug mode

Running silently:

> ./verbose.py   
> 
johnsyweb
  • 136,902
  • 23
  • 188
  • 247
78

Here is a more concise method, that does bounds checking, and will list valid values in help:

parser = argparse.ArgumentParser(description='This is a demo.')
parser.add_argument("-l", "--log", dest="logLevel", choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], help="Set the logging level")

args = parser.parse_args()
if args.logLevel:
    logging.basicConfig(level=getattr(logging, args.logLevel))

Usage:

demo.py --log DEBUG
Ztyx
  • 14,100
  • 15
  • 78
  • 114
Stickley
  • 4,561
  • 3
  • 30
  • 29
  • 29
    There's no need for `getattr`: `level=logging.getLevelName(args.logLevel)` – xmedeko Sep 02 '16 at 05:30
  • 20
    Even better is to add `default='INFO'` to `add_argument()` as well as `(default: %(default)s)` to the help text, then delete the `if args.logLevel` and always call `basicConfig()`. Then the user can know what the default value will always be. – jfritz42 Mar 08 '17 at 18:05
37

Another variant would be to count the number of -v and use the count as an index to the a list of logging levels:

import argparse
import logging
    
parser = argparse.ArgumentParser()
parser.add_argument('-v', '--verbose', action='count', default=0)
args = parser.parse_args()

levels = [logging.WARNING, logging.INFO, logging.DEBUG]
level = levels[min(args.verbose, len(levels) - 1)]  # cap to last level index
logging.basicConfig(level=level)
    
logging.debug('debug message')
logging.info('info message')
logging.warning('warning message')
logging.error('error message')
logging.critical('critical message')
  • If no option is provided then logging.WARNING is selected.
  • If one option -v is provided then logging.INFO is selected.
  • If two options -v -v (or -vv) or more are provided then logging.DEBUG is selected.
Géry Ogam
  • 6,336
  • 4
  • 38
  • 67
RubenLaguna
  • 21,435
  • 13
  • 113
  • 151
  • 4
    Nice! Just remember to run your script as `python script.py -v [other_args]`. If you run `python -v script.py` (`-v` before the script), you will get a lot of debug output from the Python interpreter itself, but not from your script. (Not a problem with the answer, just a warning to other readers). – logc Sep 21 '16 at 12:59
  • 1
    Good one! This is the most flexible (multiple logging levels), clean (doesn't pollute the arguments namespace) and intuitive (used by several other tools) solution. – m000 Nov 21 '19 at 13:15
  • Keep in mind that this only affects the `logging` (which is `stderr`) and not the communication with the user (`stdout` usually via `print()`). Also note the [Python-Docs about when to use logging and when to use print](https://docs.python.org/3/howto/logging.html#when-to-use-logging). IMHO `-v` shouldn't affect the logging level but only the `stdout` output. – buhtz Jan 04 '23 at 13:29
8

You can explicity specify a level as an integer after the -v flag:

parser = argparse.ArgumentParser()
parser.add_argument("-v", "--verbose", const=1, default=0, type=int, nargs="?",
                    help="increase verbosity: 0 = only warnings, 1 = info, 2 = debug. No number means info. Default is no verbosity.")
args = parser.parse_args()

logger = logging.getLogger()
if args.verbose == 0:
    logger.setLevel(logging.WARN) 
elif args.verbose == 1:
    logger.setLevel(logging.INFO) 
elif args.verbose == 2:
    logger.setLevel(logging.DEBUG) 
iacopo
  • 663
  • 1
  • 7
  • 22
Stephan
  • 372
  • 3
  • 7
2

if you want to enable logging.DEBUG level for a script you don't want to (or cannot) edit, you can customize your startup:

jcomeau@aspire:~$ python -c "import site; site._script()"
[snip]...
USER_BASE: '/home/jcomeau/.local' (exists)
USER_SITE: '/home/jcomeau/.local/lib/python2.7/site-packages' (exists)
ENABLE_USER_SITE: True
jcomeau@aspire:~$ mkdir -p ~/.local/lib/python2.7/site-packages
jcomeau@aspire:~$ vi ~/.local/lib/python2.7/site-packages/usercustomize.py

enter the following:

import os, logging
if os.getenv('DEBUGGING'):
    logging.basicConfig(level = logging.DEBUG)

then you can just:

jcomeau@aspire:~$ mkdir -p /tmp/some/random/
jcomeau@aspire:~$ echo 'import logging; logging.debug("test")' >> /tmp/some/random/script.py
jcomeau@aspire:~$ DEBUGGING=1 python /tmp/some/random/script.py 
DEBUG:root:test

from Paul Ollis at http://nedbatchelder.com/blog/201001/running_code_at_python_startup.html


2017-07-18: I've since switched to a different method:

logging.basicConfig(level=logging.DEBUG if __debug__ else logging.INFO)

what this does is, if you're running without optimization (as in python script.py) you get the DEBUG-level stuff, whereas if you run with python -OO script.py you don't. no environment variables to set.

jcomeau_ictx
  • 37,688
  • 6
  • 92
  • 107
2

Here's another take on having argparse count the -v option to increase verbosity up two levels from the default WARNING to INFO (-v) to DEBUG (-vv). This does not map to the constants defined by logging but rather calculates the value directly, limiting the input:

print( "Verbosity / loglevel:", args.v )
logging.basicConfig( level=10*(3-max(0,min(args.v,3))) )
logging.debug("debug") # 10
logging.info("info") # 20
logging.warning("warning") # 30 - The default level is WARNING, which means that only events of this level and above will be tracked
logging.error("error") # 40
logging.critical("critical") # 50
handle
  • 5,859
  • 3
  • 54
  • 82
  • Note the argparse argument needs `default=0`, otherwise you'll get `TypeError: '<' not supported between instances of 'int' and 'NoneType'`. Or add `args.v or 0` above... – handle Dec 06 '19 at 09:42