AskUser
AskUser is a smart CLI utility for collecting and validating user input in Python. It wraps common prompt patterns—validation, menus, defaults, multi-selects, and autocompletion—into a simple, consistent API.
📑 Table of Contents
- Installation
- API Overview
validate_input- Menus & Options
- Database-Style Selection
- Yes/No Shortcut
- Autocomplete
- Validation Types
- Custom Validators (Extension API)
- Testing
- License
📦 Installation
pip install askuser
📖 API Overview
| Function / Class | What It Does |
|---|---|
validate_input(...) |
Prompt for free-form input, validate type/pattern, retry until valid |
pretty_menu(*args, **kwargs) |
Print a formatted menu |
validate_user_option(...) |
Show a menu and return the selected key |
validate_user_option_value(...) |
Return the selected value |
validate_user_option_enumerated(...) |
Enumerate a dict and return (key, value) |
validate_user_option_multi(...) |
Multi-select menu (returns keys) |
validate_user_option_value_multi(...) |
Multi-select menu (returns values) |
choose_from_db(...) |
Select an existing DB id from tabulated rows |
choose_dict_from_list_of_dicts(...) |
Select and return a dict |
yes(...) |
Yes/No shortcut |
user_prompt(...) |
Prompt with autocomplete |
SubstringCompleter |
Substring-based completer (advanced use) |
🔍 validate_input
validate_input(
input_msg: str,
validation_type: str | Literal[
'custom','required','not_in','none_if_blank','yes_no',
'int','float','decimal','alpha','alphanum','custom_chars','regex',
'date','future_date','time','url','slug','email','phone','language'
],
expected_inputs: list = None,
not_in: list = None,
maximum: int | float = None,
minimum: int | float = None,
allowed_chars: str = None,
allowed_regex: str = None,
default: Any = None
) -> Union[str,int,float,None]
Behavior
- Defaults
- If
defaultis set, pressing Enter returns the default.
- If
- Automatic hints
yes_no→(y/n)none_if_blank→(optional)time→(hh:mm:ss)maximum/minimum→(max: …)/(min: …)default→(default: …)
- Validation types
- Built-in:
int,float,decimal,alpha,alphanum,date,future_date,time,url,email,phone,slug,language. - List/pattern helpers:
customwithexpected_inputs=[...]not_inwithnot_in=[...]custom_charswithallowed_chars="abc123"regexwithallowed_regex="^[A-Z]+$"
- Built-in:
- Errors
- Invalid input raises internally and the user is re-prompted.
Example
count = validate_input(
"How many items?",
"int",
minimum=1,
maximum=100,
default=10
)
🧭 Menus & Options
pretty_menu(*args, **kwargs)
Prints a menu without prompting:
pretty_menu("List", "Add", d="Delete", q="Quit")
Output:
0: List 1: Add d: Delete q: Quit
Keys are case-sensitive. What you see is what you type.
validate_user_option(...)
validate_user_option(
input_msg: str = "Option:",
*args,
**kwargs # pass q=False to suppress quit
) -> Any
- Auto-adds
q: quitunlessq=False - Returns the selected key
- For
**kwargs - For
*args, keys are enumerated strings ("0","1", …)
opt = validate_user_option("Pick:", "Red", "Blue", g="Green")
# keys: '0','1','g','q'
opt = validate_user_option("Pick:", "One", "Two", q=False)
# keys: '0','1'
validate_user_option_value(...)
- Builds same menu, returns the value.
- No
qby default (legacy behavior).
genre = validate_user_option_value(a="Action", c="Comedy", d="Drama")
# 'c' → "Comedy"
validate_user_option_multi(...)
- Multi-select version of
validate_user_option. - Exit with
d: doneby default. Ifdis already used in your options, exit appears asxd, orxd2,xd3, ... - Pass
d=Falseto disable exit and force “pick until exhausted.” - Returns a list of keys in the order picked.
STATUS = {0: "new", 1: "active", 7: "rejected"}
picked = validate_user_option_multi("Select statuses:", **STATUS)
# → [1, 7]
- Exit with
d: done(orxd,xd2, … ifdis taken) - Pass
d=Falseto force selection until exhausted
validate_user_option_value_multi(...)
- Multi-select version of
validate_user_option_value.
vals = validate_user_option_value_multi(
"Pick genres",
a="Action",
c="Comedy",
d="Drama"
)
# user picks: c, a → ['Comedy', 'Action']
validate_user_option_enumerated(dict, msg="Option:", start=1)
validate_user_option_enumerated(
a_dict: dict,
msg: str = "Option:",
start: int = 1
) -> tuple
- Enumerates
.items()starting atstart. - Adds
q: quit. - Returns
(key, value)or('q', None).
movies = {101: "Inception", 202: "Memento"}
mid, title = validate_user_option_enumerated(movies, start=1)
- Enumerates dict items
- Adds
q: quit - Returns
(key, value)or('q', None)
🗄 Database-Style Selection
choose_from_db(db_result, input_msg=None, table_desc=None, xq=False)
choose_from_db(
db_result: list[dict],
input_msg: str = None,
table_desc: str = None,
xq: bool = False
) -> tuple
- Pretty-prints rows with
tabulate. - Only existing
idvalues indb_resultare valid. - If
xq=True, also acceptsxq→ returns('xq', 'quit'). - Invalid entries re-prompt.
choose_dict_from_list_of_dicts(list_of_dicts, key_to_choose)
choose_dict_from_list_of_dicts(
list_of_dicts: list[dict],
key_to_choose: str
) -> dict
- Menu of
dict[key_to_choose]. - Returns selected dict.
fruits = [
{"name": "Apple", "color": "red"},
{"name": "Banana", "color": "yellow"},
]
choice = choose_dict_from_list_of_dicts(fruits, "name")
✅ Yes/No Shortcut
yes("Continue?", default="y") # True if 'y', False if 'n'; blank → default
💬 Autocomplete
from askuser.autocomplete import user_prompt
country = user_prompt("Country: ", ["USA", "UK", "IN"])
# typing ≥2 chars will start showing suggestions from list
With dicts:
- return_value=True returns the value (defaults to False, returning the key)
opts = {"usa": "United States", "uk": "United Kingdom"}
val = user_prompt("Code: ", opts, return_value=True)
print(val)
# Code: usa
# United States
🔎 Validation Types
This table reflects actual runtime behavior, including case handling.
| Type | Description |
|---|---|
required |
Must not be blank |
none_if_blank |
Blank input returns None |
yes_no |
Accepts y / n (case-insensitive, normalized to lowercase) |
int |
Integer with optional minimum / maximum |
float |
Float with optional minimum / maximum |
decimal |
Decimal with optional bounds |
alpha |
Alphabetic characters only |
alphanum |
Alphanumeric characters only |
date |
YYYY-MM-DD or YYYY-MM-DD HH:MM:SS |
future_date |
Date must be today or in the future |
time |
HH:MM:SS |
email |
RFC-compliant email |
phone |
Digits with optional + (spaces/dashes stripped) |
url |
Hostname with optional path/query |
slug |
Lowercased [a-z0-9-], deduplicated delimiter |
language |
ISO-639 via pycountry |
custom |
Exact match against expected_inputs (case-sensitive) |
not_in |
Reject values in not_in (case-insensitive comparison) |
custom_chars |
Only characters in allowed_chars |
regex |
Must match provided regex |
Design note: Case-sensitivity is intentional.
If you want case-insensitive behavior forcustom, normalize input yourself or register a custom validator.
🧩 Custom Validators (Extension API)
AskUser supports registering additional validators at runtime without mutating internal globals directly.
from askuser import register_validator, validate_input
def is_even(user_input: str) -> int:
n = int(user_input)
if n % 2:
raise ValueError("Must be even")
return n
register_validator("even", is_even)
x = validate_input("Enter even number:", "even")
Helpers available:
get_validators()register_validator(name, func, overwrite=False)register_validators({name: func, ...}, overwrite=False)unregister_validator(name)
🧪 Testing
Under tests/, examples:
import pytest
from askuser import validate_input, yes, validate_user_option
def test_default(monkeypatch):
monkeypatch.setattr('builtins.input', lambda _: '')
assert validate_input("Prompt?", "int", default=7) == 7
def test_no_quit(monkeypatch):
monkeypatch.setattr('builtins.input', lambda _: '0')
assert validate_user_option("Pick:", "A", "B", q=False) == '0'
Run:
pytest tests/
📜 License
MIT — free to use, modify, and distribute.
Metadata
Release files for askuser 0.2.5
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| askuser-0.2.5.tar.gz | 24.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| askuser-0.2.5-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 43.0 kB
Release files / askuser-0.2.5.tar.gz
| Download URL | askuser-0.2.5.tar.gz |
|---|---|
| Size | 24.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
22739167140308213ac169e601cbe600e36f3e3ff82a0e6b5742498dbcc2a615
|
|
BLAKE2b-256 checksum How to use checksums |
0944ec0322c3da7f8f51e1ebf79848c3e350a712a33cb5f0dbe97c755939edf0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.0
|
Release files / askuser-0.2.5-py3-none-any.whl
| Download URL | askuser-0.2.5-py3-none-any.whl |
|---|---|
| Size | 19.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
500c8c51529863a73e4b3c978b7f6b142e72399d6e18a540843d98e4417006f6
|
|
BLAKE2b-256 checksum How to use checksums |
d77c2f129072e1fb3b97a8bbf5cd8a89681408668ef179ca5dc6664c4691f9d5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.0
|