0
# Forms and Multi-Question Workflows
1
2
Forms enable multi-question workflows that execute a series of prompts and return consolidated results, supporting field validation, conditional logic, and both synchronous and asynchronous execution.
3
4
## Capabilities
5
6
### Form Creation
7
8
Create forms from multiple Question instances with named fields for organized data collection.
9
10
```python { .api }
11
def form(**kwargs: Question) -> Form:
12
"""
13
Create a form from keyword arguments mapping field names to Questions.
14
15
Args:
16
**kwargs: Field names mapped to Question instances
17
18
Returns:
19
Form instance ready for execution
20
"""
21
22
class FormField(NamedTuple):
23
key: str
24
question: Question
25
"""
26
Named tuple representing a question within a form.
27
28
Attributes:
29
key: Field identifier for result dictionary
30
question: Question instance to execute
31
"""
32
```
33
34
### Form Execution
35
36
Execute forms with comprehensive error handling and result consolidation.
37
38
```python { .api }
39
class Form:
40
def __init__(self, *form_fields: FormField) -> None:
41
"""
42
Initialize form with FormField instances.
43
44
Args:
45
*form_fields: FormField instances defining the form structure
46
"""
47
48
def ask(self, patch_stdout: bool = False,
49
kbi_msg: str = DEFAULT_KBI_MESSAGE) -> Dict[str, Any]:
50
"""
51
Execute form synchronously with error handling.
52
53
Args:
54
patch_stdout: Patch stdout to prevent interference
55
kbi_msg: Message displayed on keyboard interrupt
56
57
Returns:
58
Dictionary mapping field keys to user responses
59
60
Raises:
61
KeyboardInterrupt: User cancelled with appropriate message
62
"""
63
64
def unsafe_ask(self, patch_stdout: bool = False) -> Dict[str, Any]:
65
"""
66
Execute form synchronously without error handling.
67
68
Args:
69
patch_stdout: Patch stdout to prevent interference
70
71
Returns:
72
Dictionary mapping field keys to user responses
73
"""
74
75
def ask_async(self, patch_stdout: bool = False,
76
kbi_msg: str = DEFAULT_KBI_MESSAGE) -> Dict[str, Any]:
77
"""
78
Execute form asynchronously with error handling.
79
80
Args:
81
patch_stdout: Patch stdout to prevent interference
82
kbi_msg: Message displayed on keyboard interrupt
83
84
Returns:
85
Dictionary mapping field keys to user responses
86
"""
87
88
def unsafe_ask_async(self, patch_stdout: bool = False) -> Dict[str, Any]:
89
"""
90
Execute form asynchronously without error handling.
91
92
Args:
93
patch_stdout: Patch stdout to prevent interference
94
95
Returns:
96
Dictionary mapping field keys to user responses
97
"""
98
```
99
100
## Usage Examples
101
102
### Basic Form Creation
103
104
```python
105
import questionary
106
107
# Create form using keyword arguments
108
user_info = questionary.form(
109
name=questionary.text("Enter your name:"),
110
email=questionary.text("Enter your email:"),
111
age=questionary.text("Enter your age:"),
112
confirmed=questionary.confirm("Is this information correct?")
113
).ask()
114
115
print(f"Name: {user_info['name']}")
116
print(f"Email: {user_info['email']}")
117
print(f"Age: {user_info['age']}")
118
print(f"Confirmed: {user_info['confirmed']}")
119
```
120
121
### Advanced Form with Validation
122
123
```python
124
import questionary
125
from questionary import Validator, ValidationError
126
127
# Custom validators
128
class EmailValidator(Validator):
129
def validate(self, document):
130
if "@" not in document.text:
131
raise ValidationError(message="Please enter a valid email")
132
133
class AgeValidator(Validator):
134
def validate(self, document):
135
try:
136
age = int(document.text)
137
if age < 0 or age > 120:
138
raise ValidationError(message="Please enter a valid age (0-120)")
139
except ValueError:
140
raise ValidationError(message="Please enter a number")
141
142
# Form with validated fields
143
registration = questionary.form(
144
username=questionary.text(
145
"Choose username:",
146
validate=lambda x: "Username too short" if len(x) < 3 else True
147
),
148
email=questionary.text(
149
"Enter email:",
150
validate=EmailValidator()
151
),
152
age=questionary.text(
153
"Enter age:",
154
validate=AgeValidator()
155
),
156
terms=questionary.confirm(
157
"Do you accept the terms?",
158
default=False
159
)
160
).ask()
161
162
if not registration['terms']:
163
print("Registration cancelled - terms not accepted")
164
else:
165
print("Registration successful!")
166
```
167
168
### Complex Form with Conditional Logic
169
170
```python
171
import questionary
172
173
# First collect basic info
174
basic_info = questionary.form(
175
user_type=questionary.select(
176
"Account type:",
177
choices=["personal", "business", "developer"]
178
),
179
name=questionary.text("Full name:")
180
).ask()
181
182
# Conditional follow-up questions based on user type
183
if basic_info['user_type'] == 'business':
184
business_info = questionary.form(
185
company=questionary.text("Company name:"),
186
employees=questionary.select(
187
"Company size:",
188
choices=["1-10", "11-50", "51-200", "200+"]
189
),
190
industry=questionary.text("Industry:")
191
).ask()
192
193
# Merge results
194
result = {**basic_info, **business_info}
195
196
elif basic_info['user_type'] == 'developer':
197
dev_info = questionary.form(
198
languages=questionary.checkbox(
199
"Programming languages:",
200
choices=["Python", "JavaScript", "Java", "C++", "Go", "Rust"]
201
),
202
experience=questionary.select(
203
"Years of experience:",
204
choices=["0-2", "3-5", "6-10", "10+"]
205
)
206
).ask()
207
208
result = {**basic_info, **dev_info}
209
210
else:
211
result = basic_info
212
213
print("Collected information:", result)
214
```
215
216
### Async Form Execution
217
218
```python
219
import questionary
220
import asyncio
221
222
async def async_form_example():
223
# Forms can be executed asynchronously
224
config = await questionary.form(
225
host=questionary.text("Database host:", default="localhost"),
226
port=questionary.text("Database port:", default="5432"),
227
database=questionary.text("Database name:"),
228
ssl=questionary.confirm("Use SSL?", default=True)
229
).ask_async()
230
231
print(f"Connecting to {config['host']}:{config['port']}")
232
return config
233
234
# Run async form
235
# config = asyncio.run(async_form_example())
236
```
237
238
### Form with Manual FormField Construction
239
240
```python
241
import questionary
242
from questionary import FormField
243
244
# Manual FormField creation for more control
245
form_fields = [
246
FormField("project_name", questionary.text("Project name:")),
247
FormField("description", questionary.text("Description:", multiline=True)),
248
FormField("license", questionary.select(
249
"License:",
250
choices=["MIT", "Apache-2.0", "GPL-3.0", "BSD-3-Clause"]
251
)),
252
FormField("public", questionary.confirm("Make repository public?"))
253
]
254
255
project_form = questionary.Form(*form_fields)
256
project_config = project_form.ask()
257
258
print("Project configuration:")
259
for key, value in project_config.items():
260
print(f" {key}: {value}")
261
```
262
263
### Error Handling in Forms
264
265
```python
266
import questionary
267
268
try:
269
settings = questionary.form(
270
debug=questionary.confirm("Enable debug mode?"),
271
log_level=questionary.select(
272
"Log level:",
273
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
274
),
275
max_connections=questionary.text(
276
"Max connections:",
277
validate=lambda x: "Must be a number" if not x.isdigit() else True
278
)
279
).ask()
280
281
print("Settings configured:", settings)
282
283
except KeyboardInterrupt:
284
print("\nConfiguration cancelled by user")
285
except Exception as e:
286
print(f"Configuration error: {e}")
287
```
288
289
### Form with Skip Logic
290
291
```python
292
import questionary
293
294
# Initial question to determine workflow
295
workflow = questionary.select(
296
"What would you like to do?",
297
choices=["create", "update", "delete"]
298
).ask()
299
300
if workflow == "create":
301
create_form = questionary.form(
302
name=questionary.text("Resource name:"),
303
type=questionary.select(
304
"Resource type:",
305
choices=["database", "server", "storage"]
306
),
307
region=questionary.select(
308
"Region:",
309
choices=["us-east-1", "us-west-2", "eu-west-1"]
310
)
311
).ask()
312
313
print(f"Creating {create_form['type']} '{create_form['name']}' in {create_form['region']}")
314
315
elif workflow == "update":
316
update_form = questionary.form(
317
resource_id=questionary.text("Resource ID to update:"),
318
changes=questionary.checkbox(
319
"What to update:",
320
choices=["configuration", "scaling", "security", "tags"]
321
)
322
).ask()
323
324
print(f"Updating resource {update_form['resource_id']}: {update_form['changes']}")
325
326
elif workflow == "delete":
327
delete_form = questionary.form(
328
resource_id=questionary.text("Resource ID to delete:"),
329
confirmation=questionary.text(
330
"Type 'DELETE' to confirm:",
331
validate=lambda x: True if x == "DELETE" else "Please type 'DELETE' to confirm"
332
)
333
).ask()
334
335
if delete_form['confirmation'] == "DELETE":
336
print(f"Deleting resource {delete_form['resource_id']}")
337
```