Master Python's argparse Module: Build Better CLIs
Python is a powerful programming language that allows developers to create a variety of applications, from basic scripts to large software systems. One of the most common programming tasks is to create command-line interfaces (CLIs), which allow users to interact with programs through a terminal or command line. CLIs are essential for scripting, automation, and instances where a graphical user interface is impractical.
Handling command-line parameters is an important part of designing CLIs. These arguments allow users to influence a program's behavior by giving input parameters as it runs. Without the ability to assess these factors, programs would be less versatile and harder to adjust for specific tasks.
Python provides several modules for parsing command-line arguments, but the argparse
module stands out for its simplicity and comprehensiveness. The argparse
module makes it easy to develop functional command-line interfaces. It automatically handles parameter parsing, displays helpful instructions, and provides errors when users provide incorrect information.
In this tutorial, we'll look at how to use the Python argparse
package. We'll start with an overview of Python's command-line interfaces and the importance of parsing command-line input. Then we'll discuss the argparse module and its benefits.
Overview of Command-Line Interfaces in Python
Command-line interfaces are programs that run purely on text-based commands. Users enter commands into a terminal or command line, which typically include arguments and options that change the program's behavior. CLIs are useful because they are easily automated and integrated with other technologies.
In Python, a CLI is created by developing a script that accepts command-line input. This input is usually presented as a list of arguments that the script can access and interpret. For basic programs, manually handling these arguments via the sys.argv list may suffice. However, when the program's complexity increases, manually processing arguments becomes inefficient and error-prone.
Importance of Parsing Command-Line Arguments
Parsing command-line arguments is necessary for numerous reasons:
- Flexibility: By taking parameters, a program can do multiple tasks or act on different data sets without changing the code. Users can specify files to process, configure settings, and select modes of operation.
- User-friendliness: Proper argument parsing enables the program to display useful messages, instruct the user on how to use the program, and gracefully handle faults.
- Maintainability: Using a dedicated parsing module, such as argparse, makes your code clearer and easier to maintain. It separates the logic used to parse arguments from the program's main operation.
Introduction to the argparse Module and Its Benefits
The argparse
module is part of Python's standard library. Therefore, it can be used without installing any additional packages. It offers a straightforward and consistent interface for parsing command line inputs. Some of the advantages of utilizing argparse
are:
- Automatic help generation: It generates help and usage messages based on the code's arguments.
- Error handling: It displays helpful error messages when users enter invalid arguments.
- Type conversion: It can automatically convert parameter strings to the appropriate data type.
- It supports both necessary positional arguments and optional arguments with simplicity.
- Default values: It allows you to provide default values for parameters that the user does not supply.
argparse enables developers to focus on the main functionality of their program while depending on a strong foundation for managing command-line input.
Setting Up argparse and Basic Usage
Now that we've covered the importance of parsing command-line arguments and the advantages of using argparse
, let's look at how to set it up and use it in a Python script.
Installing and importing argparse
There is no need to install anything separately because argparse
is part of Python's standard library. You can import it right at the start of your script.
import argparse
This line loads the argparse
module into your script, allowing you to use its functionality to parse command-line arguments.
Creating a simple argument parser
The first step in utilizing argparse
is to generate a parser object. This object will store information about the arguments your program accepts and parse command-line input.
parser = argparse.ArgumentParser(description='Process some integers.')
In this example, we create an ArgumentParser()
object and describe the program. When the user selects the help option (-h
or --help
), this description will be displayed.
Adding positional and optional arguments
Now that we've developed the parser, we can specify the arguments our program will accept.
Adding positional arguments
Positional arguments are essential and must be presented in a precise order. For example, consider a script that adds two numbers. We can define two positional arguments for the numbers:
parser.add_argument('num1', type=int, help='The first number to add.')
parser.add_argument('num2', type=int, help='The second number to add.')
In this code, num1
and num2
refer to the positional parameters.
type=int
indicates that the parameters should be converted to integers. The help argument specifies a description that will appear in the help message.
Adding optional arguments
Optional arguments are not necessary and typically provide additional options or change the behavior of the program. They are usually prefixed with one or two dashes. We'll add an optional argument to enable verbose output:
parser.add_argument('-v', '--verbose', action='store_true', help='Increase output verbosity.')
Here:
- The short option is
-v
(for example,-v
).--verbose
is a lengthy option. action='store_true'
indicates that if the option is selected, the verbose attribute will be set toTrue
; otherwise, it will beFalse
.- The
help
argument specifies a description for the help message.
Parsing arguments and accessing their values
After specifying all of the arguments, we need to parse the command-line input. Use the .parse_args()
method to achieve this.
args = parser.parse_args()
The parsed parameters are now stored in the args
variable as attributes. We can access them using dot notation.
result = args.num1 + args.num2
print('The sum of {} and {} is {}'.format(args.num1, args.num2, result))
if args.verbose:
print('Verbose mode is enabled.')
Putting everything together, here's a complete script that adds two numbers and includes an optional verbose mode:
import argparse
parser = argparse.ArgumentParser(description='Add two integers.')
parser.add_argument('num1', type=int, help='The first number to add.')
parser.add_argument('num2', type=int, help='The second number to add.')
parser.add_argument('-v', '--verbose', action='store_true', help='Increase output verbosity.')
args = parser.parse_args()
result = args.num1 + args.num2
print('The sum of {} and {} is {}'.format(args.num1, args.num2, result))
if args.verbose:
print('Calculation completed successfully.')
You can run this script from the command line and provide the required positional arguments:
python add_numbers.py 3 5
The sum of 3 and 5 is 8
If you include the -v
or --verbose
option, the script will print the additional verbose message:
python add_numbers.py 3 5 --verbose
The sum of 3 and 5 is 8
Calculation completed successfully.
If the user runs the script with the -h
or --help
option, argparse
will display an automatically generated help message:
python add_numbers.py -h
usage: add_numbers.py [-h] [-v] num1 num2
Add two integers.
positional arguments:
num1 The first number to add.
num2 The second number to add.
optional arguments:
-h, --help show this help message and exit
-v, --verbose Increase output verbosity.
This feature makes your program more user-friendly by providing clear instructions on how to use it.
Advanced Argument Handling
When developing command-line programs in Python, you may encounter scenarios requiring more complex argument parsing. Python's argparse
module includes several features to address these complex requirements, allowing you to develop flexible and user-friendly interfaces.
Using nargs for multiple arguments
There are situations when your application needs to accept numerous values for the same argument. For example, assume you wish to create a script that processes multiple files at once. The nargs parameter in argparse
allows you to specify how many command-line arguments should be read.
Here's how to use nargs
to accept multiple filenames:
import argparse
parser = argparse.ArgumentParser(description='Process multiple files.')
parser.add_argument('filenames', nargs='+', help='List of files to process.')
args = parser.parse_args()
for filename in args.filenames:
print(f'Processing file: {filename}')
# Add your file processing code here
In this case, nargs='+'
instructs the parser to expect one or more arguments in filenames. The user can provide as many file names as necessary, and they will be saved in a list called args.filenames
.
If you want to accept a certain amount of arguments, set nargs
to that number. For example, nargs=2
requires exactly two parameters.
Implementing choices to limit argument values
Sometimes, you want to limit an argument to a specified range of valid values. This guarantees that the user offers valid input, hence avoiding mistakes or unexpected actions. The options parameter lets you specify the allowable values for an argument.
Consider a script that executes multiple activities depending on a mode selected by the user.
import argparse
parser = argparse.ArgumentParser(description='Perform actions in different modes.')
parser.add_argument('--mode', choices=['backup', 'restore', 'delete'], required=True, help='Mode of operation.')
args = parser.parse_args()
if args.mode == 'backup':
print('Backing up data...')
# Backup code here
elif args.mode == 'restore':
print('Restoring data...')
# Restore code here
elif args.mode == 'delete':
print('Deleting data...')
# Delete code here
In this script, the --mode
argument must be one of the options. If the user enters a value that is not in the list, argparse
will return an error message.
Handling boolean flags and toggles
Boolean flags are choices that enable or disable specific functionalities in your application. They are typically defined without a value, merely by including the flag in the command. You can handle these flags in argparse
with the action
parameter.
For instance, let's include a debug mode in a script:
import argparse
parser = argparse.ArgumentParser(description='A script with debug mode.')
parser.add_argument('--debug', action='store_true', help='Enable debug output.')
args = parser.parse_args()
if args.debug:
print('Debug mode is enabled.')
# Additional debug information here
else:
print('Debug mode is disabled.')
By using action='store_true'
, the --debug
flag will set args.debug
to True
when present and False
otherwise.
Setting default values and required arguments
Optional arguments frequently include sensible default values. This signifies that if the user does not specify the argument, the application will use the default. The default
argument allows you to specify a default value.
Here's an example:
import argparse
parser = argparse.ArgumentParser(description='Adjust program settings.')
parser.add_argument('--timeout', type=int, default=30, help='Timeout in seconds.')
args = parser.parse_args()
print(f'Timeout is set to {args.timeout} seconds.')
In this scenario, if the user does not specify --timeout
, the default is 30 seconds.
To make an optional argument mandatory, set required=True
.
import argparse
parser = argparse.ArgumentParser(description='Send a message.')
parser.add_argument('--recipient', required=True, help='Recipient of the message.')
args = parser.parse_args()
print(f'Sending message to {args.recipient}.')
The script will now require the --recipient
argument.
Customizing Help and Error Messages
Providing clear and helpful messages to users is an essential component of developing an effective command-line program. Python's argparse
module automatically creates help messages, but you can modify these messages to better suit your needs.
Generating automatic help messages
By default, argparse
generates a help message, which may be accessed using the -h
or --help
options. This message contains the program's usage, a description, and information about each argument.
For example:
import argparse
parser = argparse.ArgumentParser(description='Calculate factorial of a number.')
parser.add_argument('number', type=int, help='The number to calculate the factorial for.')
args = parser.parse_args()
When a user runs the script with -h
, they will see:
usage: script.py [-h] number
Calculate factorial of a number.
positional arguments:
number The number to calculate the factorial for.
optional arguments:
-h, --help show this help message and exit
This automatic help message gives useful information without requiring any additional effort.
Customizing help descriptions and usage messages
While the default help messages are useful, you may want to alter them to provide extra information or to fit a specific structure. You can change the description, epilog, and usage text in the ArgumentParser
.
For example, to include an epilog and personalize the usage message:
import argparse
parser = argparse.ArgumentParser(
description='Convert temperatures between Celsius and Fahrenheit.',
epilog='Enjoy using the temperature converter!',
usage='%(prog)s [options] temperature')
parser.add_argument('temperature', type=float, help='Temperature value to convert.')
parser.add_argument('--to-fahrenheit', action='store_true', help='Convert Celsius to Fahrenheit.')
parser.add_argument('--to-celsius', action='store_true', help='Convert Fahrenheit to Celsius.')
args = parser.parse_args()
Now, when the user checks the help message, it will include the customized description, usage, and epilog:
python file.py --help
usage: p.py [options] temperature
Convert temperatures between Celsius and Fahrenheit.
positional arguments:
temperature Temperature value to convert.
options:
-h, --help show this help message and exit
--to-fahrenheit Convert Celsius to Fahrenheit.
--to-celsius Convert Fahrenheit to Celsius.
Managing error handling and user feedback
If a user enters invalid arguments, argparse
will display an error message and exit the program. You can modify this behavior to provide more useful feedback or to handle failures differently.
One approach is to override the error method in a subclass of ArgumentParser
:
import argparse
import sys
class CustomArgumentParser(argparse.ArgumentParser):
def error(self, message):
print(f'Error: {message}')
self.print_help()
sys.exit(2)
parser = CustomArgumentParser(description='Divide two numbers.')
parser.add_argument('numerator', type=float, help='The numerator.')
parser.add_argument('denominator', type=float, help='The denominator.')
args = parser.parse_args()
if args.denominator == 0:
parser.error('Denominator cannot be zero.')
result = args.numerator / args.denominator
print(f'Result: {result}')
If the user attempts to divide by zero in this script, the application will display an error warning and help text, directing the user to provide valid data.
Python file.py 6 0
Error: Denominator cannot be zero.
usage: file.py [-h] numerator denominator
Divide two numbers.
positional arguments:
numerator The numerator.
denominator The denominator.
options:
-h, --help show this help message and exit
You can also include custom error handling in your script. For instance, to manage invalid file paths:
import argparse
import os
parser = argparse.ArgumentParser(description='Read a file and display its contents.')
parser.add_argument('filepath', help='Path to the file.')
args = parser.parse_args()
if not os.path.exists(args.filepath):
parser.error(f"The file {args.filepath} does not exist.")
with open(args.filepath, 'r') as file:
contents = file.read()
print(contents)
Running the script with an invalid path will display the error below:
python app..py file
usage: p.py [-h] filepath
app.py: error: The file file does not exist.
Real-World Examples and Use Cases
Understanding how to use the argparse
module in real-world settings will make its functionality clearer. Let's look at some instances of how to use argparse
in real-world applications.
Building a command-line calculator
Assume you need to develop a simple calculator that can do basic arithmetic operations from the command line. This calculator should accept two numbers and an operator to execute the requested computation.
Here's how to approach this task:
import argparse
parser = argparse.ArgumentParser(description='Simple command-line calculator.')
parser.add_argument('num1', type=float, help='First number.')
parser.add_argument('operator', choices=['+', '-', '*', '/'], help='Operation to perform.')
parser.add_argument('num2', type=float, help='Second number.')
args = parser.parse_args()
if args.operator == '+':
result = args.num1 + args.num2
elif args.operator == '-':
result = args.num1 - args.num2
elif args.operator == '*':
result = args.num1 * args.num2
elif args.operator == '/':
if args.num2 == 0:
print('Error: Division by zero is not allowed.')
exit(1)
result = args.num1 / args.num2
print(f'The result is: {result}')
In this script, the argparse
module is used to define three positional arguments: two numbers and an operator. The choices
argument limits the operator to valid arithmetic symbols. When the user runs the script, they can perform these calculations:
python calculator.py 10 + 5
The result is: 15.0
This basic calculator shows how command-line options can enhance a program's flexibility and interactivity.
Creating a file-processing script with multiple options
Assume you require a script that processes text files and provides choices such as designating an output file, selecting a processing mode, and enabling verbose output.
Here's an example of how you could set it up:
import argparse
parser = argparse.ArgumentParser(description='Process text files.')
parser.add_argument('input_file', help='Path to the input file.')
parser.add_argument('-o', '--output', help='Path to the output file.')
parser.add_argument('-m', '--mode', choices=['uppercase', 'lowercase'], default='uppercase', help='Processing mode.')
parser.add_argument('-v', '--verbose', action='store_true', help='Enable verbose output.')
args = parser.parse_args()
# Read the input file
with open(args.input_file, 'r') as file:
content = file.read()
if args.verbose:
print(f'Reading from {args.input_file}')
# Process the content
if args.mode == 'uppercase':
processed_content = content.upper()
else:
processed_content = content.lower()
if args.verbose:
print('Processing content')
# Write to the output file or print to console
if args.output:
with open(args.output, 'w') as file:
file.write(processed_content)
if args.verbose:
print(f'Writing output to {args.output}')
else:
print(processed_content)
This script accepts an input file and has options for the output file, processing mode, and verbose output. Users can modify how the script behaves without altering the code.
python text_processor.py input.txt -o output.txt --mode lowercase -v
Reading from input.txt
Processing content
Writing output to output.txt
Developing a CLI tool with subcommands
In more complicated applications, subcommands may be required, similar to how git works using commands such as git commit and git push. The argparse
module provides subparsers for this purpose.
Here's how to make a CLI tool with subcommands:
import argparse
parser = argparse.ArgumentParser(description='Manage tasks.')
subparsers = parser.add_subparsers(dest='command', required=True)
# Subcommand 'add'
parser_add = subparsers.add_parser('add', help='Add a new task.')
parser_add.add_argument('name', help='Name of the task.')
parser_add.add_argument('-p', '--priority', type=int, choices=range(1, 6), default=3, help='Priority of the task.')
# Subcommand 'list'
parser_list = subparsers.add_parser('list', help='List all tasks.')
parser_list.add_argument('-a', '--all', action='store_true', help='List all tasks, including completed ones.')
# Subcommand 'complete'
parser_complete = subparsers.add_parser('complete', help='Mark a task as completed.')
parser_complete.add_argument('task_id', type=int, help='ID of the task to complete.')
args = parser.parse_args()
if args.command == 'add':
print(f"Adding task '{args.name}' with priority {args.priority}")
# Code to add the task
elif args.command == 'list':
print('Listing tasks')
if args.all:
print('Including completed tasks')
# Code to list tasks
elif args.command == 'complete':
print(f'Marking task {args.task_id} as completed')
# Code to complete the task
In this example, the script has three subcommands: add
, list
, and complete
. Each subcommand has its arguments. When users run the script, they enter the subcommand and any other parameters.
For example:
python task_manager.py add "Write report" -p 2
Adding task 'Write report' with priority 2
Listing tasks
python task_manager.py list
Listing tasks
Marking tasks as completed:
python task_manager.py complete 3
Marking task 3 as completed
subparsers
enable you to create complex command line tools that are well organized and easy to extend, allowing you to build applications that can do multiple things in a single interface.
Python argparse Best Practices and Tips
Developing command-line programs entails more than simply writing code that works. It also involves writing code that is clean, maintainable, and user-friendly.
Here are some best practices when working with the argparse
module.
Organizing code for readability and maintenance
When your scripts become more complex, it's critical to keep your code organized for better understanding and maintenance. One approach to accomplish this is to use functions and classes to organize distinct sections of your code.
Breaking down your program into smaller, reusable chunks makes it easier to manage and eliminates code duplication.
For example, in a task management script, you could define different functions for adding tasks, listing tasks, and completing tasks. This division enables you to concentrate on one aspect of the logic at a time, making your code clearer.
Another effective technique is to separate your argument parsing logic from the rest of your code. Placing all argument definitions and parsing at the start of your script, or in a dedicated function, makes it easier for others to comprehend how your program handles input.
Using appropriate variable names also improves readability. Choose variable names that reflect their purpose, so that anyone viewing your code can understand what is going on. Including comments and docstrings
to explain what your functions do and any key details can help to increase comprehension.
Testing and debugging command-line applications
Testing your applications is critical to ensuring they function properly and detecting faults early on. Writing unit tests with Python testing frameworks such as unittest
or pytest
is an excellent approach to testing your code. These tests allow you to mimic various inputs and ensure that your functions work properly.
For example, you can test multiple scenarios by mocking the command-line inputs rather than running the script from the command line. This strategy allows you to ensure that your argument parsing works as expected and that your application handles various scenarios correctly.
Handling exceptions gracefully is also vital. Using try-except
blocks, you can catch errors and deliver useful messages to the user. This makes your application more stable and user-friendly.
Consider including a debug flag in your script as well. This flag can enable additional output that allows you to track the program's execution when something goes wrong. Having this option makes it easier to diagnose problems throughout development and maintenance.
Comparing argparse with other argument parsing libraries
While argparse
is a valuable utility in Python's standard library, there are additional libraries available that provide alternative methods for parsing command-line arguments. Understanding these options will help you select the right tool for your project.
One such library is Click. Click is a third-party package for creating command-line interfaces with decorators. It has a more intuitive syntax and is ideal for sophisticated applications. For example, you can use Python decorators to create commands and options, making your code more compact and understandable.
Docopt is another option that allows you to define your command-line interface using the program's docstring. Docopt automatically parses the help message and constructs the argument parser when the usage instructions are included in the docstring. This approach is elegant and suitable for simple scripts that require a more human-readable specification.
When picking which library to utilize, think about your project's demands. If you wish to eliminate external dependencies and want a tool that can handle most circumstances, argparse is an excellent solution. If you want a more intuitive syntax and are working on a complex application, Click could be better suited. Docopt is a good choice for small programs with plain interfaces.
Conclusion
Throughout this article, we've looked at how to use the Python argparse
module to develop command-line programs in Python. Working through real-world examples, we learned how to create realistic scripts that receive user input and accomplish relevant tasks.
From designing a basic calculator to developing a task management tool with subcommands, the argparse
module provides the versatility required to handle a wide range of circumstances. You can design applications that are dependable and simple to maintain by adhering to best practices such as code organization, comprehensive testing, and consideration of alternative libraries.
Whether you're automating activities, analyzing data, or creating complicated tools, understanding command-line argument parsing improves your ability to design effective Python programs. Boost your Python knowledge today with our Python Fundamentals skill track.
Python argparse FAQs
How can I make an optional argument required in argparse?
Optional arguments in argparse are typically not necessary because they are, by definition, optional. However, there may be instances where you want an argument beginning with -- to be mandatory. To accomplish this, set the required parameter to True
when adding the argument.
Here's how you can accomplish it:
import argparse
parser = argparse.ArgumentParser(description='Process some data.')
parser.add_argument('--input', required=True, help='Path to the input file.')
args = parser.parse_args()
print(f'Input file: {args.input}')
How do I handle mutually exclusive arguments with argparse?
Sometimes you may encounter arguments that should not be used together. For example, a script may take either --verbose or --quiet, but not both at once. To tackle this scenario, argparse supports the definition of mutually exclusive groups.
Here is how to set it up:
import argparse
parser = argparse.ArgumentParser(description='Process some data.')
group = parser.add_mutually_exclusive_group()
group.add_argument('--verbose', action='store_true', help='Enable verbose output.')
group.add_argument('--quiet', action='store_true', help='Enable quiet mode.')
args = parser.parse_args()
if args.verbose:
print('Verbose mode is on.')
elif args.quiet:
print('Quiet mode is on.')
else:
print('Default mode is on.')
This script allows the user to enable either verbose or quiet mode, but not both. If they attempt to utilize both options simultaneously, argparse will throw an error.
How can I use argparse to read arguments from a file instead of the command line?
While argparse is intended for parsing command-line arguments, you may want to read them from a file. You can do this by reading the arguments from the file and giving them to the parse_args()
method via the args parameter.
Here's an example.
import argparse
parser = argparse.ArgumentParser(description='Process some data.')
parser.add_argument('--input', help='Path to the input file.')
parser.add_argument('--output', help='Path to the output file.')
# Read arguments from a file
with open('args.txt', 'r') as file:
file_args = file.read().split()
args = parser.parse_args(file_args)
print(f'Input file: {args.input}')
print(f'Output file: {args.output}')
In this case, the args.txt file might contain:
--input data/input.txt --output data/output.txt
The script reads the parameters from the file, converts them into a list, and then gives them to parse_args(). This allows you to manage your arguments in a file rather than inputting them every time.
How do I customize the help message in argparse to show default values?
By default, argparse does not display the parameters' default values in the help message. If you want to include default values in the help text, either change the help parameter or use the ArgumentDefaultsHelpFormatter.
Here's how to add default values to the help message:
import argparse
parser = argparse.ArgumentParser(
description='Process some data.',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('--timeout', type=int, default=60, help='Timeout in seconds.')
args = parser.parse_args()
print(f'Timeout is set to {args.timeout} seconds.')
With the ArgumentDefaultsHelpFormatter, the help message will display the default value for the --timeout argument:
usage: script.py [-h] [--timeout TIMEOUT]
Process some data.
optional arguments:
-h, --help show this help message and exit
--timeout TIMEOUT Timeout in seconds. (default: 60)
This provides users with more information about the default settings of your program.
What are some best practices for testing command-line applications that use argparse?
Testing command-line apps can be difficult, but there are methods to make it easier. One useful way is to replicate command-line arguments using the unittest module.
Here's an example of how to test a script using argparse:
import unittest
import argparse
import sys
def create_parser():
parser = argparse.ArgumentParser(description='Process some data.')
parser.add_argument('--number', type=int, help='A number.')
return parser
def main(args):
parser = create_parser()
parsed_args = parser.parse_args(args)
if parsed_args.number is None:
parser.error('No number provided.')
result = parsed_args.number * 2
return result
class TestMain(unittest.TestCase):
def test_main_with_number(self):
test_args = ['--number', '5']
result = main(test_args)
self.assertEqual(result, 10)
def test_main_without_number(self):
test_args = []
with self.assertRaises(SystemExit):
main(test_args)
if __name__ == '__main__':
unittest.main()
In the test script:
- The .create_parser() method initializes the argument parser.
- The main() function processes the arguments and executes the main logic.
- The TestMain class offers test methods for simulating various command-line inputs.
- The tests ensure that the program responds as expected to diverse inputs.
Structure your code to separate the argument processing and primary logic, making it easier to test different areas of your program.
tutorial
Argument Parsing in Python
tutorial
How to Run Python Scripts Tutorial
tutorial
An Introduction to Python Subprocess: Basics and Examples
tutorial
30 Cool Python Tricks For Better Code With Examples
tutorial
Python NiceGUI: Build Powerful Web Interfaces with Ease
Laiba Siddiqui
9 min
tutorial