Of course! Building a form builder in Python is a common task, especially for web applications. The "best" way to do it depends heavily on your use case:

- For the Web: Building a UI that allows users to create forms, save them, and then have other users fill them out.
- For Scripting/CLI: Creating a program that generates Python code or configuration for a form library (like Django Forms).
- For Desktop Apps: Building a graphical form builder using a framework like PyQt or Tkinter.
I'll cover the most popular and practical approach: building a web-based form builder. We'll use Flask for the backend and WTForms for the form handling logic, as it's a clean and powerful combination.
This guide will be broken down into two main parts:
- Part 1: The Form Builder (Admin)
- A UI to add/remove fields (Text, Email, Number, etc.).
- Save the form definition to a database.
- Part 2: The Form Renderer (Public User)
- Load the saved form definition from the database.
- Dynamically render the form so a user can fill it out.
Core Concepts
- Form Definition: We need a way to store the structure of a form. This will be our "blueprint". It will include things like the field type, label, and any validation rules.
- Form Instance: This is the actual, populated form that a user sees and interacts with.
- Form Builder UI: The interface where an admin creates the form definition.
- Form Renderer UI: The page where the public user fills out the form.
Part 1: Building the Form Builder (Admin Interface)
Let's create a simple web app where an admin can build a form called "User Feedback".
Step 1: Project Setup
First, let's set up our project structure and install the necessary libraries.

# Create a project directory mkdir python_form_builder cd python_form_builder # Create virtual environment python -m venv venv source venv/bin/activate # On Windows: venv\Scripts\activate # Install libraries pip install Flask Flask-SQLAlchemy Flask-WTF
Your project structure should look like this:
python_form_builder/
├── app.py
├── venv/
└── templates/
└── ...
Step 2: Define the Form Definition Model
We need a database table to store our form blueprints. We'll use SQLAlchemy for this.
# app.py
from flask import Flask, render_template, redirect, url_for, flash
from flask_sqlalchemy import SQLAlchemy
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, TextAreaField, IntegerField, SelectField
from wtforms.validators import DataRequired, Email, NumberRange
app = Flask(__name__)
# Secret key is needed for session management and CSRF protection
app.config['SECRET_KEY'] = 'a-very-secret-key'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///form_builder.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
# --- Database Models ---
# This model stores the definition of a single form (e.g., "User Feedback")
class FormDefinition(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False, unique=True)
fields = db.Column(db.JSON, nullable=False) # Store fields as a JSON list
# This model stores the data submitted by a user for a specific form definition
class FormData(db.Model):
id = db.Column(db.Integer, primary_key=True)
form_definition_id = db.Column(db.Integer, db.ForeignKey('form_definition.id'), nullable=False)
data = db.Column(db.JSON, nullable=False) # Store submitted data as a JSON object
# You could add a timestamp, user_id, etc.
# --- WTForms for Building the Form ---
class FormBuilderForm(FlaskForm):
form_name = StringField('Form Name', validators=[DataRequired()])
# This field will hold the JSON representation of our form fields
form_structure = TextAreaField('Form Structure (JSON)', validators=[DataRequired()])
submit = SubmitField('Create Form Definition')
# --- WTForms for Rendering the Form ---
# This class will be dynamically populated
class DynamicRenderedForm(FlaskForm):
pass # Fields will be added at runtime
# --- Routes ---
@app.route('/')
def index():
return render_template('index.html')
@app.route('/build', methods=['GET', 'POST'])
def build_form():
form = FormBuilderForm()
if form.validate_on_submit():
try:
# You would parse the JSON here and validate it
# For simplicity, we'll just save the raw JSON string
form_def = FormDefinition(
name=form.form_name.data,
fields=form.form_structure.data
)
db.session.add(form_def)
db.session.commit()
flash(f'Successfully created form definition: {form.form_name.data}!', 'success')
return redirect(url_for('list_forms'))
except Exception as e:
flash(f'Error creating form: {e}', 'danger')
return render_template('build_form.html', form=form)
@app.route('/forms')
def list_forms():
forms = FormDefinition.query.all()
return render_template('list_forms.html', forms=forms)
@app.route('/form/<int:form_id>/fill', methods=['GET', 'POST'])
def fill_form(form_id):
form_def = FormDefinition.query.get_or_404(form_id)
# Dynamically create the form class from the JSON definition
form_data = eval(form_def.fields) # WARNING: eval() is dangerous! Use a proper JSON parser.
class F(FlaskForm):
pass
for field_config in form_data['fields']:
field_type = field_config['type']
label = field_config['label']
name = field_config['name']
validators = []
if field_config.get('required'):
validators.append(DataRequired())
if field_type == 'StringField':
setattr(F, name, StringField(label, validators=validators))
elif field_type == 'EmailField':
setattr(F, name, StringField(label, validators=[DataRequired(), Email()]))
elif field_type == 'IntegerField':
min_val = field_config.get('min')
max_val = field_config.get('max')
v = [DataRequired(), NumberRange(min=min_val, max=max_val)]
setattr(F, name, IntegerField(label, validators=v))
# Add more field types as needed
rendered_form = F()
if rendered_form.validate_on_submit():
# Collect data into a dictionary
submitted_data = {name: rendered_form[name].data for name in rendered_form.data}
# Save to database
form_entry = FormData(
form_definition_id=form_id,
data=submitted_data
)
db.session.add(form_entry)
db.session.commit()
flash('Form submitted successfully!', 'success')
return redirect(url_for('list_forms'))
return render_template('fill_form.html', form=rendered_form, form_name=form_def.name)
# --- Main execution ---
if __name__ == '__main__':
with app.app_context():
db.create_all() # Create database tables
app.run(debug=True)
Step 3: Create the HTML Templates
Create a templates folder and add the following files.
templates/base.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">Python Form Builder</title>
<style>
body { font-family: sans-serif; margin: 2em; }
.container { max-width: 800px; margin: auto; }
.flash { padding: 1em; margin-bottom: 1em; border-radius: 5px; }
.flash.success { background-color: #d4edda; color: #155724; }
.flash.danger { background-color: #f8d7da; color: #721c24; }
nav { margin-bottom: 2em; }
nav a { margin-right: 1em; text-decoration: none; }
fieldset { border: 1px solid #ccc; padding: 1em; border-radius: 5px; }
label { display: block; margin-bottom: 0.5em; font-weight: bold; }
input, textarea { width: 100%; padding: 0.5em; margin-bottom: 1em; box-sizing: border-box; }
button { padding: 0.7em 1.5em; background-color: #007bff; color: white; border: none; border-radius: 5px; cursor: pointer; }
</style>
</head>
<body>
<div class="container">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="flash {{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
</body>
</html>
templates/index.html
{% extends "base.html" %}
{% block content %}
<h1>Welcome to the Form Builder</h1>
<nav>
<a href="{{ url_for('build_form') }}">Build a New Form</a>
<a href="{{ url_for('list_forms') }}">View Existing Forms</a>
</nav>
{% endblock %}
templates/build_form.html
{% extends "base.html" %}
{% block content %}
<h1>Build a New Form</h1>
<form method="POST" action="">
{{ form.hidden_tag() }}
<fieldset>
<div>
{{ form.form_name.label }}
{{ form.form_name() }}
</div>
<div>
{{ form.form_structure.label }}
{{ form.form_structure(rows=10, cols=50) }}
<small>Provide a JSON object defining your form. Example:</small>
<pre>{
"name": "User Feedback",
"fields": [
{"name": "user_name", "type": "StringField", "label": "Your Name", "required": true},
{"name": "user_email", "type": "EmailField", "label": "Your Email", "required": true},
{"name": "rating", "type": "IntegerField", "label": "Rating (1-5)", "required": true, "min": 1, "max": 5},
{"name": "feedback", "type": "TextAreaField", "label": "Your Feedback"}
]
}</pre>
</div>
<div>
{{ form.submit() }}
</div>
</fieldset>
</form>
{% endblock %}
templates/list_forms.html
{% extends "base.html" %}
{% block content %}
<h1>Available Forms</h1>
<nav>
<a href="{{ url_for('build_form') }}">Build a New Form</a>
</nav>
<ul>
{% for form in forms %}
<li>
<strong>{{ form.name }}</strong>
(<a href="{{ url_for('fill_form', form_id=form.id) }}">Fill out this form</a>)
</li>
{% endfor %}
</ul>
{% endblock %}
templates/fill_form.html
{% extends "base.html" %}
{% block content %}
<h1>Fill out: {{ form_name }}</h1>
<form method="POST" action="">
{{ form.hidden_tag() }}
<fieldset>
{% for field in form %}
{% if field.type != 'HiddenField' %}
<div>
{{ field.label }}
{{ field() }}
{% if field.errors %}
<span style="color: red;">{{ field.errors[0] }}</span>
{% endif %}
</div>
{% endif %}
{% endfor %}
<div>
<button type="submit">Submit</button>
</div>
</fieldset>
</form>
{% endblock %}
Step 4: Run the Application
- Make sure you are in the
python_form_builderdirectory and your virtual environment is active. - Run the Flask app:
flask run
- Open your browser and go to
http://127.0.0.1:5000.
How to Use It
- Build a Form: Click "Build a New Form". Copy the example JSON into the "Form Structure" field, give it a name like "User Feedback", and click "Create Form Definition".
- View Forms: Click "View Existing Forms". You should see "User Feedback" in the list.
- Fill out the Form: Click the "Fill out this form" link next to "User Feedback". You will see a dynamically generated form with fields for Name, Email, Rating, and Feedback.
- Submit: Fill out the form and click "Submit". You'll get a success message.
Improvements and Considerations
-
Security (CRITICAL): The
eval()function is extremely dangerous. In a real application, you should never useeval()on user-provided data. Instead, use a safer approach:- Create a parser that validates the JSON structure against a known schema.
- Use a library like
jsonschemato validate the input. - Have a whitelist of allowed field types and attributes.
- The
eval()here is used for simplicity in this example. Replace it with a proper, safe parser.
-
UI/UX for Building: The current "builder" is just a text area. A real builder would have a drag-and-drop interface where you could add fields, configure them, and see a live preview. This would require JavaScript (e.g., with a library like React or Vue.js on the frontend).
-
More Field Types: You can easily extend the
fill_formroute to support more WTForms field types likeBooleanField,DateField,SelectField, etc. -
Database Choice: SQLite is great for development. For a production application, you would use a more robust database like PostgreSQL or MySQL.
-
User Authentication: Right now, any user can fill out any form and see all forms. A real application would have user authentication (e.g., with Flask-Login) to manage who can build forms and who can view submitted data.
