📝 Forms Handling¶
Processing user input from HTML forms with Python
What are HTML Forms?¶
HTML Forms allow users to submit data to a web server. They're essential for: - User registration and login - Search functionality - Data entry and editing - File uploads - Feedback and contact
Think of forms like paper forms at a doctor's office: - Fields to fill out (name, age, symptoms) - A submit button to hand it in - The staff processes the information
HTML Form Basics¶
<form action="/submit" method="POST">
<label for="name">Name:</label>
<input type="text" id="name" name="name" required>
<label for="email">Email:</label>
<input type="email" id="email" name="email" required>
<button type="submit">Submit</button>
</form>
Key Attributes:
| Attribute | Purpose | Example |
|---|---|---|
action |
URL where form data is sent | /submit, /register |
method |
HTTP method (GET or POST) | GET, POST |
name |
Field identifier (sent to server) | name="username" |
required |
Makes field mandatory | required |
GET vs POST¶
GET Method¶
- Data sent in URL (query parameters)
- Visible in browser address bar
- Limited amount of data (~2048 chars)
- Good for: Search, filters, bookmarkable pages
- Not for: Passwords, sensitive data
POST Method¶
- Data sent in request body
- Not visible in URL
- Can send large amounts of data
- Good for: Login forms, data creation, file uploads
- Required for: Passwords, sensitive information
POST /login HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 35
username=alice&password=secret123
Form Data Encoding¶
application/x-www-form-urlencoded¶
Default encoding. Key-value pairs joined with &:
Special characters are URL-encoded:
- Space → + or %20
- @ → %40
- & → %26
- = → %3D
multipart/form-data¶
Used for file uploads. More complex format with boundaries.
Parsing Form Data in Python¶
From GET Requests (Query String)¶
from urllib.parse import urlparse, parse_qs
class FormHandler(BaseHTTPRequestHandler):
def do_GET(self):
# Parse URL
parsed = urlparse(self.path)
path = parsed.path
query_string = parsed.query
# Parse query parameters
params = parse_qs(query_string)
# Convert single-item lists to values
data = {k: v[0] if len(v) == 1 else v for k, v in params.items()}
# data = {'search': 'python', 'page': '1'}
From POST Requests (Request Body)¶
from urllib.parse import parse_qs
class FormHandler(BaseHTTPRequestHandler):
def do_POST(self):
# Get content length from headers
content_length = int(self.headers.get('Content-Length', 0))
# Read the request body
post_data = self.rfile.read(content_length)
# Parse form data
params = parse_qs(post_data.decode('utf-8'))
# Convert to simple dict
data = {k: v[0] if len(v) == 1 else v for k, v in params.items()}
# Process the data
self.process_form(data)
Complete Form Handling Example¶
"""
forms_server.py - Complete form handling example
"""
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs
from html import escape
class FormHandler(BaseHTTPRequestHandler):
def do_GET(self):
"""Serve the form page."""
if self.path == '/':
self.serve_form()
elif self.path.startswith('/result'):
self.serve_result_get()
else:
self.send_error(404)
def do_POST(self):
"""Process form submission."""
if self.path == '/submit':
self.process_form_post()
else:
self.send_error(404)
def serve_form(self):
"""Display the contact form."""
html = """<!DOCTYPE html>
<html>
<head>
<title>Contact Form</title>
<style>
body { font-family: Arial, sans-serif; max-width: 500px; margin: 50px auto; }
.field { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
input, textarea { width: 100%; padding: 8px; border: 1px solid #ddd; }
button { background: #007bff; color: white; padding: 10px 20px;
border: none; cursor: pointer; }
button:hover { background: #0056b3; }
</style>
</head>
<body>
<h1>Contact Us</h1>
<form action="/submit" method="POST">
<div class="field">
<label for="name">Name:</label>
<input type="text" id="name" name="name" required>
</div>
<div class="field">
<label for="email">Email:</label>
<input type="email" id="email" name="email" required>
</div>
<div class="field">
<label for="message">Message:</label>
<textarea id="message" name="message" rows="4" required></textarea>
</div>
<button type="submit">Send Message</button>
</form>
</body>
</html>"""
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(html.encode())
def process_form_post(self):
"""Process POST form submission."""
# Read form data
content_length = int(self.headers.get('Content-Length', 0))
post_data = self.rfile.read(content_length)
# Parse form data
params = parse_qs(post_data.decode('utf-8'))
data = {k: v[0] for k, v in params.items()}
# Extract fields
name = data.get('name', '')
email = data.get('email', '')
message = data.get('message', '')
# Validate
errors = []
if not name:
errors.append("Name is required")
if not email or '@' not in email:
errors.append("Valid email is required")
if not message:
errors.append("Message is required")
if errors:
self.serve_error(errors)
else:
# Success - show confirmation
self.serve_success(name, email, message)
def serve_success(self, name, email, message):
"""Show success page."""
html = f"""<!DOCTYPE html>
<html>
<head><title>Success</title></head>
<body style="font-family: Arial; max-width: 500px; margin: 50px auto;">
<h1>✅ Message Sent!</h1>
<p>Thank you, <strong>{escape(name)}</strong>!</p>
<p>We've received your message and will reply to {escape(email)}.</p>
<h3>Your Message:</h3>
<blockquote style="background: #f5f5f5; padding: 15px; border-left: 4px solid #007bff;">
{escape(message)}
</blockquote>
<a href="/">← Back to Form</a>
</body>
</html>"""
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(html.encode())
def serve_error(self, errors):
"""Show error page."""
errors_html = ''.join(f'<li>{escape(e)}</li>' for e in errors)
html = f"""<!DOCTYPE html>
<html>
<head><title>Error</title></head>
<body style="font-family: Arial; max-width: 500px; margin: 50px auto;">
<h1>❌ Error</h1>
<p>Please fix the following issues:</p>
<ul style="color: #dc3545;">{errors_html}</ul>
<a href="/">← Back to Form</a>
</body>
</html>"""
self.send_response(400)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(html.encode())
def log_message(self, format, *args):
print(f"[{self.date_time_string()}] {args[0]}")
if __name__ == '__main__':
server = HTTPServer(('localhost', 8000), FormHandler)
print('Form server running at http://localhost:8000')
server.serve_forever()
Form Validation¶
Always validate form data on the server:
def validate_form(data):
"""Validate form data and return errors list."""
errors = []
# Required fields
required = ['name', 'email', 'message']
for field in required:
if not data.get(field, '').strip():
errors.append(f"{field.title()} is required")
# Email format
email = data.get('email', '')
if email and '@' not in email:
errors.append("Invalid email format")
# Length limits
name = data.get('name', '')
if len(name) > 100:
errors.append("Name is too long (max 100 characters)")
# Type checking
age = data.get('age', '')
if age:
try:
age = int(age)
if age < 0 or age > 150:
errors.append("Age must be between 0 and 150")
except ValueError:
errors.append("Age must be a number")
return errors
Handling Different Input Types¶
def parse_input(value, input_type):
"""Parse form value based on input type."""
if input_type == 'checkbox':
# Checkbox: present = True, absent = False
return value == 'on'
elif input_type == 'number':
try:
return int(value)
except ValueError:
try:
return float(value)
except ValueError:
return None
elif input_type == 'date':
from datetime import datetime
try:
return datetime.strptime(value, '%Y-%m-%d').date()
except ValueError:
return None
elif input_type == 'select-multiple':
# Multiple select returns list
return value if isinstance(value, list) else [value]
else:
# text, email, password, etc.
return value.strip()
Common Mistakes¶
| Mistake | Why It's Wrong | Correct Approach |
|---|---|---|
| Trusting client-side validation | Can be bypassed | Always validate on server |
| Not escaping output | XSS vulnerability | Use html.escape() |
| Using GET for sensitive data | Visible in URL/history | Use POST for passwords |
| Not checking Content-Length | Could read forever | Limit and validate size |
| Storing passwords in plain text | Security risk | Hash with bcrypt/argon2 |
| Not handling encoding | Unicode errors | Always specify UTF-8 |
Quick Reference¶
from urllib.parse import parse_qs
from html import escape
# Parse POST form data
content_length = int(self.headers.get('Content-Length', 0))
post_data = self.rfile.read(content_length)
data = parse_qs(post_data.decode('utf-8'))
# Convert to simple dict
form = {k: v[0] if len(v) == 1 else v for k, v in data.items()}
# Access fields
name = form.get('name', '')
email = form.get('email', '')
# Validate
if not name:
errors.append("Name required")
# Escape for display
safe_name = escape(name)
Next Steps¶
Congratulations! You've completed Module 20: Web Basics. You now know how to:
- Write HTML and CSS
- Build HTTP servers with Python
- Route URLs to handlers
- Generate dynamic templates
- Handle form submissions
Where to go next: - Practice by building a complete web application - Learn about cookies and sessions - Explore Flask or Django for production applications - Study web security in depth