1. Pengenalan Argparse
argparse adalah modul bawaan Python (standard library) untuk membuat command-line interface (CLI). Argparse menyediakan parsing argumen, auto-generate help messages, dan validasi input dari pengguna.
Mengapa Argparse?
| Fitur | argparse | sys.argv | click |
|---|---|---|---|
| Built-in | ✅ Ya | ✅ Ya | ❌ Perlu install |
| Auto help | ✅ --help | ❌ Manual | ✅ Ya |
| Type checking | ✅ Otomatis | ❌ Manual | ✅ Dekorator |
| Error messages | ✅ Jelas | ❌ Manual | ✅ Jelas |
| Subcommands | ✅ Ya | ❌ Manual | ✅ Ya |
| Kompleksitas | Sedang | Rendah | Rendah |
┌──────────────────────────────────────────────────────────────┐ │ ANATOMI CLI ARGUMENTS │ │ │ │ $ mytool -v --name "Budi" copy source.txt dest.txt │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └──────┴──────────┘ │ │ │ │ │ │ Positional Args │ │ │ │ │ │ │ │ │ │ └──────────┘ │ │ │ │ Optional Args (--name "Budi") │ │ │ │ │ │ │ └─── Flags (-v = --verbose) │ │ │ │ │ └─────── Program name │ │ │ │ $ mytool copy -f source.txt dest.txt │ │ │ │ │ └─── Subcommand │ └──────────────────────────────────────────────────────────────┘
2. Argparse Hello World
import argparse
# Buat parser
parser = argparse.ArgumentParser(description='Program sederhana')
# Tambahkan argumen
parser.add_argument('nama', help='Nama Anda')
# Parse argumen dari command line
args = parser.parse_args()
print(f"Halo, {args.nama}!")
# Jalankan dari terminal:
# $ python hello.py Budi
# Halo, Budi!
#
# $ python hello.py --help
# usage: hello.py [-h] nama
#
# Program sederhana
#
# positional arguments:
# nama Nama Anda
#
# options:
# -h, --help show this help message and exit
#
# $ python hello.py
# usage: hello.py [-h] nama
# hello.py: error: the following arguments are required: nama
3. Positional Arguments
Positional arguments wajib diisi dan dikenali berdasarkan urutan posisinya di command line.
import argparse
parser = argparse.ArgumentParser(description='Kalkulator sederhana')
# Beberapa positional arguments
parser.add_argument('angka1', type=float, help='Angka pertama')
parser.add_argument('operasi', help='Operasi: tambah, kurang, kali, bagi')
parser.add_argument('angka2', type=float, help='Angka kedua')
args = parser.parse_args()
if args.operasi == 'tambah':
print(f"Hasil: {args.angka1 + args.angka2}")
elif args.operasi == 'kurang':
print(f"Hasil: {args.angka1 - args.angka2}")
elif args.operasi == 'kali':
print(f"Hasil: {args.angka1 * args.angka2}")
elif args.operasi == 'bagi':
if args.angka2 == 0:
print("Error: Tidak bisa membagi dengan nol!")
else:
print(f"Hasil: {args.angka1 / args.angka2}")
# Usage:
# $ python kalk.py 10 tambah 5
# Hasil: 15.0
#
# $ python kalk.py 20 kali 3
# Hasil: 60.0
nargs: Multiple Values
import argparse
parser = argparse.ArgumentParser(description='Contoh nargs')
# nargs='+' — satu atau lebih nilai
parser.add_argument('files', nargs='+', help='Satu atau lebih file')
# nargs='*' — nol atau lebih nilai
parser.add_argument('patterns', nargs='*', help='Pattern opsional')
# nargs='?' — nol atau satu nilai
parser.add_argument('output', nargs='?', default='output.txt', help='File output')
# nargs=N — tepat N nilai
# parser.add_argument('coords', nargs=3, type=float, help='x y z')
args = parser.parse_args()
print(f"Files: {args.files}")
print(f"Patterns: {args.patterns}")
print(f"Output: {args.output}")
# Usage:
# $ python contoh.py file1.txt file2.txt --help
# $ python contoh.py *.py
# $ python contoh.py file1.txt file2.txt output.csv
4. Optional Arguments
Optional arguments dimulai dengan - (short form) atau -- (long form) dan bersifat opsional.
import argparse
parser = argparse.ArgumentParser(description='Backup tool')
# Short dan long form
parser.add_argument('-s', '--source', required=True, help='Folder sumber')
parser.add_argument('-d', '--dest', default='./backup', help='Folder tujuan (default: ./backup)')
# Boolean flag (store_true / store_false)
parser.add_argument('-v', '--verbose', action='store_true', help='Mode verbose')
parser.add_argument('--no-compress', action='store_true', help='Kompresi dinonaktifkan')
# Count flags (-vvv = 3)
parser.add_argument('-V', '--verbosity', action='count', default=0, help='Tingkat verbosity (-V, -VV, -VVV)')
# Multiple values
parser.add_argument('-e', '--exclude', action='append', help='Folder yang dikecualikan (bisa beberapa)')
# Const value
parser.add_argument('--mode', nargs='?', const='default_mode', default=None, help='Mode operasi')
args = parser.parse_args()
print(f"Source: {args.source}")
print(f"Dest: {args.dest}")
print(f"Verbose: {args.verbose}")
print(f"Compress: {not args.no_compress}")
print(f"Verbosity level: {args.verbosity}")
print(f"Exclude: {args.exclude}")
# Usage:
# $ python backup.py -s /data -d /backup -v
# $ python backup.py --source /data --verbose --exclude .git --exclude __pycache__
# $ python backup.py -s /data -VVV
Argument Actions
| Action | Fungsi | Contoh |
|---|---|---|
store | Simpan nilai (default) | --name Budi → args.name = "Budi" |
store_true | Simpan True jika ada flag | -v → args.verbose = True |
store_false | Simpan False jika ada flag | --no-color → args.color = False |
count | Hitung jumlah flag | -vvv → args.verbose = 3 |
append | Append ke list | -e a -e b → args.e = ['a', 'b'] |
append_const | Append const value | Digunakan untuk mengumpulkan const values |
version | Print version | --version → myapp 1.0.0 |
5. Tipe Data dan Type Casting
import argparse
from pathlib import Path
parser = argparse.ArgumentParser(description='Contoh tipe data')
# Tipe data dasar
parser.add_argument('-n', '--name', type=str, help='Nama (string)')
parser.add_argument('-a', '--age', type=int, help='Umur (integer)')
parser.add_argument('-h', '--height', type=float, help='Tinggi (float)')
# File
parser.add_argument('-i', '--input', type=argparse.FileType('r'), help='Input file')
parser.add_argument('-o', '--output', type=argparse.FileType('w'), help='Output file')
# Path
parser.add_argument('-d', '--dir', type=Path, help='Direktori')
# Boolean parsing
parser.add_argument('--debug', type=lambda x: x.lower() in ('true', '1', 'yes'),
default=False, help='Debug mode (true/false)')
# Range validation
def range_type(min_val, max_val):
"""Buat type function untuk range validation."""
def check_range(value):
fval = float(value)
if fval < min_val or fval > max_val:
raise argparse.ArgumentTypeError(
f"Nilai harus antara {min_val} dan {max_val}"
)
return fval
return check_range
parser.add_argument('--port', type=range_type(1024, 65535),
help='Port number (1024-65535)')
# Custom validation
def positive_int(value):
"""Pastikan integer positif."""
ivalue = int(value)
if ivalue <= 0:
raise argparse.ArgumentTypeError(f"'{value}' harus positif")
return ivalue
parser.add_argument('--count', type=positive_int, help='Jumlah (harus positif)')
args = parser.parse_args()
print(f"Name: {args.name}")
print(f"Age: {args.age}")
print(f"Port: {args.port}")
6. Choices dan Validation
import argparse
parser = argparse.ArgumentParser(description='File converter')
# Choices: membatasi nilai yang valid
parser.add_argument('format', choices=['json', 'csv', 'xml', 'yaml'],
help='Format output (json/csv/xml/yaml)')
parser.add_argument('-c', '--compress', choices=['gzip', 'bz2', 'lzma', 'none'],
default='none', help='Metode kompresi')
# Choices dengan range
parser.add_argument('-l', '--level', type=int, choices=range(0, 10),
default=5, help='Level kompresi (0-9)')
# Required optional argument
parser.add_argument('-i', '--input', required=True, help='File input')
# Default value
parser.add_argument('-o', '--output', default=None, help='File output')
# metavar: tampilkan nama custom di help
parser.add_argument('-u', '--user', metavar='USERNAME', help='Username')
parser.add_argument('-p', '--password', metavar='PASS', help='Password')
args = parser.parse_args()
print(f"Format: {args.format}")
print(f"Input: {args.input}")
print(f"Compress: {args.compress}")
print(f"Level: {args.level}")
# Usage:
# $ python converter.py json -i data.txt
# $ python converter.py csv -i data.txt --compress gzip -l 9
# $ python converter.py invalid -i data.txt
# error: argument format: invalid choice: 'invalid' (choose from 'json', 'csv', 'xml', 'yaml')
7. Subcommands
Subcommands memungkinkan Anda membuat CLI dengan perintah seperti git commit, git push, docker run, dll.
import argparse
def cmd_init(args):
"""Handler untuk subcommand 'init'."""
print(f"Menginisialisasi project: {args.nama}")
print(f"Template: {args.template}")
def cmd_build(args):
"""Handler untuk subcommand 'build'."""
print(f"Building project...")
print(f"Output: {args.output}")
print(f"Verbose: {args.verbose}")
if args.watch:
print("Mode watch aktif!")
def cmd_deploy(args):
"""Handler untuk subcommand 'deploy'."""
print(f"Deploying ke: {args.target}")
print(f"Environment: {args.env}")
# Main parser
parser = argparse.ArgumentParser(description='CLI Tool untuk Development')
parser.add_argument('-V', '--version', action='version', version='%(prog)s 1.0.0')
subparsers = parser.add_subparsers(title='Perintah', dest='command',
help='Perintah yang tersedia')
# Subcommand: init
init_parser = subparsers.add_parser('init', help='Inisialisasi project baru')
init_parser.add_argument('nama', help='Nama project')
init_parser.add_argument('-t', '--template', default='basic',
choices=['basic', 'api', 'fullstack', 'cli'],
help='Template project (default: basic)')
init_parser.set_defaults(func=cmd_init)
# Subcommand: build
build_parser = subparsers.add_parser('build', help='Build project')
build_parser.add_argument('-o', '--output', default='./dist', help='Folder output')
build_parser.add_argument('-v', '--verbose', action='store_true', help='Verbose output')
build_parser.add_argument('-w', '--watch', action='store_true', help='Watch mode')
build_parser.set_defaults(func=cmd_build)
# Subcommand: deploy
deploy_parser = subparsers.add_parser('deploy', help='Deploy project')
deploy_parser.add_argument('target', choices=['staging', 'production'],
help='Target deployment')
deploy_parser.add_argument('-e', '--env', default='.env',
help='File environment')
deploy_parser.set_defaults(func=cmd_deploy)
args = parser.parse_args()
if hasattr(args, 'func'):
args.func(args)
else:
parser.print_help()
# Usage:
# $ python mycli.py init myapp --template api
# $ python mycli.py build -v --watch
# $ python mycli.py deploy staging
# $ python mycli.py --help
# $ python mycli.py init --help
Nested Subcommands
import argparse
parser = argparse.ArgumentParser(description='Database CLI')
subparsers = parser.add_subparsers(dest='command')
# db create
db_parser = subparsers.add_parser('db', help='Database operations')
db_sub = db_parser.add_subparsers(dest='db_command')
create_parser = db_sub.add_parser('create', help='Create database')
create_parser.add_argument('name', help='Database name')
create_parser.add_argument('--engine', default='sqlite',
choices=['sqlite', 'postgres', 'mysql'])
migrate_parser = db_sub.add_parser('migrate', help='Run migrations')
migrate_parser.add_argument('--revision', help='Target revision')
# Usage:
# $ python db.py db create mydb --engine postgres
# $ python db.py db migrate --revision head
8. Mutually Exclusive Groups
import argparse
parser = argparse.ArgumentParser(description='Contoh argument groups')
# Mutually exclusive: pilih salah satu
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('--json', action='store_true', help='Output format JSON')
group.add_argument('--csv', action='store_true', help='Output format CSV')
group.add_argument('--xml', action='store_true', help='Output format XML')
# Mutually exclusive dengan value
mode_group = parser.add_mutually_exclusive_group()
mode_group.add_argument('--create', metavar='NAME', help='Buat baru')
mode_group.add_argument('--delete', metavar='NAME', help='Hapus')
mode_group.add_argument('--update', metavar='NAME', help='Update')
# Argument group (untuk help display, bukan eksklusif)
output_group = parser.add_argument_group('Output options')
output_group.add_argument('-o', '--output', help='File output')
output_group.add_argument('-q', '--quiet', action='store_true', help='Quiet mode')
output_group.add_argument('-v', '--verbose', action='count', default=0, help='Verbose')
args = parser.parse_args()
if args.json:
print("Format: JSON")
elif args.csv:
print("Format: CSV")
elif args.xml:
print("Format: XML")
# Usage:
# $ python contoh.py --json
# $ python contoh.py --csv --create mydata
# $ python contoh.py --json --csv ← ERROR: not allowed
9. Custom Type dan Action
import argparse
import os
from pathlib import Path
# --- Custom Type Functions ---
def existing_file(value):
"""Validasi bahwa file ada."""
path = Path(value)
if not path.exists():
raise argparse.ArgumentTypeError(f"File tidak ditemukan: {value}")
if not path.is_file():
raise argparse.ArgumentTypeError(f"Bukan file: {value}")
return path
def dir_or_create(value):
"""Validasi direktori, buat jika tidak ada."""
path = Path(value)
if not path.exists():
path.mkdir(parents=True, exist_ok=True)
return path
def ip_address(value):
"""Validasi IP address sederhana."""
parts = value.split('.')
if len(parts) != 4:
raise argparse.ArgumentTypeError(f"IP tidak valid: {value}")
for part in parts:
if not part.isdigit() or not 0 <= int(part) <= 255:
raise argparse.ArgumentTypeError(f"IP tidak valid: {value}")
return value
def port_number(value):
"""Validasi port number."""
ivalue = int(value)
if not 1 <= ivalue <= 65535:
raise argparse.ArgumentTypeError(f"Port harus 1-65535, dapat: {value}")
return ivalue
# --- Custom Action ---
class EnvDefault(argparse.Action):
"""Action yang mengambil default dari environment variable."""
def __init__(self, env_var, required=True, default=None, **kwargs):
self.env_var = env_var
default = os.environ.get(env_var, default)
if default:
required = False
super().__init__(default=default, required=required, **kwargs)
def __call__(self, parser, namespace, values, option_string=None):
setattr(namespace, self.dest, values)
# --- Parser ---
parser = argparse.ArgumentParser(description='Custom types demo')
parser.add_argument('-c', '--config', type=existing_file,
help='File konfigurasi (harus ada)')
parser.add_argument('-o', '--output-dir', type=dir_or_create,
default='./output', help='Direktori output')
parser.add_argument('--host', type=ip_address,
default='127.0.0.1', help='IP address server')
parser.add_argument('--port', type=port_number,
default=8080, help='Port server')
parser.add_argument('--db-url', action=EnvDefault, env_var='DATABASE_URL',
help='Database URL (default: $DATABASE_URL)')
args = parser.parse_args()
print(f"Config: {args.config}")
print(f"Output dir: {args.output_dir}")
print(f"Server: {args.host}:{args.port}")
print(f"DB URL: {args.db_url}")
10. Contoh Aplikasi Nyata
Contoh 1: File Manager CLI
#!/usr/bin/env python3
"""File Manager CLI — contoh argparse nyata."""
import argparse
import os
import shutil
from pathlib import Path
from datetime import datetime
def cmd_list(args):
"""List files in directory."""
target = Path(args.path)
if not target.exists():
print(f"Error: '{args.path}' tidak ditemukan")
return
files = sorted(target.iterdir(), key=lambda f: f.name)
if args.type == 'files':
files = [f for f in files if f.is_file()]
elif args.type == 'dirs':
files = [f for f in files if f.is_dir()]
total_size = 0
for f in files:
if f.is_file():
size = f.stat().st_size
total_size += size
if args.human:
size_str = format_size(size)
else:
size_str = str(size)
mod_time = datetime.fromtimestamp(f.stat().st_mtime).strftime('%Y-%m-%d %H:%M')
print(f"{size_str:>10} {mod_time} {f.name}")
else:
print(f"{'DIR':>10} {'':>16} {f.name}/")
print(f"\nTotal: {len(files)} item(s)")
if args.human:
print(f"Size: {format_size(total_size)}")
def cmd_find(args):
"""Find files matching pattern."""
root = Path(args.path)
pattern = args.pattern
count = 0
for f in root.rglob(pattern):
if args.type == 'files' and not f.is_file():
continue
if args.type == 'dirs' and not f.is_dir():
continue
print(f" {f.relative_to(root)}")
count += 1
print(f"\nDitemukan: {count} item(s)")
def cmd_clean(args):
"""Clean temporary files."""
target = Path(args.path)
patterns = ['*.pyc', '__pycache__', '*.tmp', '.DS_Store', 'Thumbs.db']
if args.custom_pattern:
patterns.extend(args.custom_pattern)
removed = 0
for pattern in patterns:
for f in target.rglob(pattern):
if args.dry_run:
print(f" [DRY RUN] Akan hapus: {f}")
else:
if f.is_file():
f.unlink()
elif f.is_dir():
shutil.rmtree(f)
print(f" Dihapus: {f}")
removed += 1
if args.dry_run:
print(f"\n[DRY RUN] Akan menghapus {removed} item(s)")
else:
print(f"\nDihapus {removed} item(s)")
def format_size(size):
"""Format size ke human-readable."""
for unit in ['B', 'KB', 'MB', 'GB']:
if size < 1024:
return f"{size:.1f}{unit}"
size /= 1024
return f"{size:.1f}TB"
def main():
parser = argparse.ArgumentParser(
prog='filemgr',
description='🗂️ File Manager CLI — kelola file dari terminal',
epilog='Contoh: filemgr list -p ~/Documents --human'
)
parser.add_argument('-V', '--version', action='version', version='filemgr 2.0.0')
subparsers = parser.add_subparsers(title='Perintah', dest='command')
# List
list_p = subparsers.add_parser('list', aliases=['ls'], help='List file dan folder')
list_p.add_argument('-p', '--path', default='.', help='Path target (default: .)')
list_p.add_argument('-t', '--type', choices=['all', 'files', 'dirs'], default='all')
list_p.add_argument('--human', '-H', action='store_true', help='Human-readable size')
list_p.set_defaults(func=cmd_list)
# Find
find_p = subparsers.add_parser('find', help='Cari file berdasarkan pattern')
find_p.add_argument('pattern', help='Pattern glob (*.py, *.txt, dll)')
find_p.add_argument('-p', '--path', default='.', help='Path root pencarian')
find_p.add_argument('-t', '--type', choices=['all', 'files', 'dirs'], default='all')
find_p.set_defaults(func=cmd_find)
# Clean
clean_p = subparsers.add_parser('clean', help='Hapus file temporary')
clean_p.add_argument('-p', '--path', default='.', help='Path target')
clean_p.add_argument('--dry-run', action='store_true', help='Preview tanpa hapus')
clean_p.add_argument('--custom-pattern', nargs='*', help='Pattern tambahan')
clean_p.set_defaults(func=cmd_clean)
args = parser.parse_args()
if hasattr(args, 'func'):
args.func(args)
else:
parser.print_help()
if __name__ == '__main__':
main()
# Usage:
# $ python filemgr.py list --human
# $ python filemgr.py find "*.py" -p ~/projects
# $ python filemgr.py clean --dry-run
# $ python filemgr.py clean -p /tmp --custom-pattern "*.log"
Contoh 2: HTTP Client CLI (Mini curl)
#!/usr/bin/env python3
"""Mini curl — contoh CLI tool dengan argparse."""
import argparse
import json
import sys
from urllib.request import Request, urlopen
from urllib.error import URLError
def main():
parser = argparse.ArgumentParser(
prog='minicurl',
description='🌐 Mini curl — HTTP client sederhana',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Contoh:
minicurl GET https://api.github.com/users/octocat
minicurl POST https://httpbin.org/post -d '{"key": "value"}'
minicurl GET https://api.github.com -H "Accept: application/json"
"""
)
parser.add_argument('method', choices=['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
help='HTTP method')
parser.add_argument('url', help='URL target')
parser.add_argument('-d', '--data', help='Request body (JSON string)')
parser.add_argument('-H', '--header', action='append',
help='Header (format: "Key: Value")')
parser.add_argument('-v', '--verbose', action='store_true', help='Verbose output')
parser.add_argument('-i', '--include-headers', action='store_true',
help='Tampilkan response headers')
parser.add_argument('-o', '--output', help='Simpan response ke file')
parser.add_argument('--timeout', type=int, default=30, help='Timeout (detik)')
args = parser.parse_args()
# Parse headers
headers = {'User-Agent': 'minicurl/1.0'}
if args.header:
for h in args.header:
key, _, value = h.partition(':')
headers[key.strip()] = value.strip()
# Parse body
data = None
if args.data:
data = args.data.encode('utf-8')
headers.setdefault('Content-Type', 'application/json')
# Buat request
req = Request(args.url, data=data, headers=headers, method=args.method)
if args.verbose:
print(f">>> {args.method} {args.url}", file=sys.stderr)
for k, v in headers.items():
print(f">>> {k}: {v}", file=sys.stderr)
if data:
print(f">>> Body: {args.data}", file=sys.stderr)
print(file=sys.stderr)
try:
with urlopen(req, timeout=args.timeout) as response:
# Response headers
if args.include_headers or args.verbose:
print(f"<<< HTTP/{response.version // 10} {response.status} {response.reason}",
file=sys.stderr)
for k, v in response.headers.items():
print(f"<<< {k}: {v}", file=sys.stderr)
print(file=sys.stderr)
# Response body
body = response.read().decode('utf-8')
# Pretty print JSON
try:
parsed = json.loads(body)
formatted = json.dumps(parsed, indent=2, ensure_ascii=False)
if args.output:
with open(args.output, 'w') as f:
f.write(formatted)
print(f"Disimpan ke {args.output}")
else:
print(formatted)
except json.JSONDecodeError:
if args.output:
with open(args.output, 'w') as f:
f.write(body)
print(f"Disimpan ke {args.output}")
else:
print(body)
except URLError as e:
print(f"Error: {e.reason}", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main()
11. Best Practices
- Selalu berikan
description— pengguna perlu tahu apa yang dilakukan tool - Gunakan
epiloguntuk contoh penggunaan - Berikan
helpuntuk setiap argument - Tentukan
defaultuntuk optional arguments - Gunakan
metavaruntuk membuat help lebih informatif - Validasi input menggunakan
typefunction atauchoices - Gunakan
set_defaults(func=...)untuk dispatcher pattern - Tambahkan
--versionuntuk tool yang didistribusikan
Tips Display
import argparse
# RawDescriptionHelpFormatter — tidak wrap teks di description/epilog
parser = argparse.ArgumentParser(
description='''Ini adalah tool CLI.
Contoh penggunaan:
tool.py --input data.csv --output result.json
tool.py -i data.csv -o result.json -v
''',
formatter_class=argparse.RawDescriptionHelpFormatter
)
# RawTextHelpFormatter — tidak wrap teks di help strings
parser = argparse.ArgumentParser(
formatter_class=argparse.RawTextHelpFormatter
)
parser.add_argument('--mode', help='''Mode operasi:
fast = Mode cepat (kurang akurat)
normal = Mode normal (default)
slow = Mode lambat (paling akurat)
''')
# ArgumentDefaultsHelpFormatter — tampilkan default di help
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter
)
parser.add_argument('-n', '--count', type=int, default=10, help='Jumlah item')
# Help: Jumlah item (default: 10)
# Metavar untuk grouped display
parser.add_argument('-o', '--output', metavar='FILE', help='Output file')
# Help: -o FILE, --output FILE Output file
12. Quiz Pemahaman
🧠 Quiz: Python Argparse
1. Apa fungsi utama argparse?
2. Bagaimana cara membuat optional argument?
3. Apa fungsi action='store_true'?
4. Bagaimana cara membuat subcommands?
5. Apa yang terjadi jika positional argument tidak diberikan?