Building Coding-Challenge: A Gamified Python Learning Platform
Educational coding platforms are everywhere, but building one from scratch teaches you invaluable lessons about security, architecture, and user experience. Coding-Challenge is my take on an interactive Python learning platform that combines secure code execution, gamification mechanics, and a thoughtful tech stack.
Project Vision and Goals
The core idea was simple: create a web application where students can:
- Solve Python programming challenges in their browser
- Get instant feedback on their code
- Earn points, achievements, and maintain daily streaks
- Track their progress over time
But beneath this simple premise lies a complex challenge: How do you safely execute untrusted code?
The Security Challenge
Why You Can't Just Run User Code
Imagine a student submits this "solution":
import os
os.system('rm -rf /') # Don't try this!
# Or worse:
while True:
os.fork() # Fork bomb
Running this directly on your server would be catastrophic. You need isolation.
The Docker Solution
Docker containers provide the perfect sandboxing mechanism. Every code submission runs in an isolated container with strict limitations:
docker run \
--rm \ # Auto-remove after execution
--network none \ # No internet access
--pids-limit 64 \ # Prevent fork bombs
--memory 64m \ # 64MB memory limit
--cpus 0.5 \ # CPU throttling
--read-only \ # Filesystem is read-only
python:3.11-slim \
python -c "$USER_CODE"
This multi-layered approach ensures:
- Malicious code can't access the network
- Fork bombs are limited to 64 processes
- Memory exhaustion is prevented
- Infinite loops are killed by timeout
- File system manipulation is impossible
Technology Stack Deep Dive
Flask: The Perfect Framework
I chose Flask 3.1.0+ over Django for several reasons:
- Minimal Boilerplate: Get started in minutes, not hours
- Educational Clarity: Small codebase (~400 lines) easy to understand
- Rich Ecosystem: Flask-Login, Flask-Mail, Flask-SQLAlchemy extensions
- Flexibility: No opinionated structure - design your own architecture
# Simple route example
@app.route('/submit/', methods=['POST'])
@login_required
def submit_solution(task_id):
code = request.form.get('code')
result = safe_execute(code, task.test_cases)
if result['all_passed']:
award_points(current_user, task.points)
check_achievements(current_user)
return jsonify(result)
MySQL + SQLAlchemy: Robust Data Management
The platform uses MySQL 8.0 with SQLAlchemy 2.0+ as the ORM. This combination provides:
- ACID Compliance: Critical for points and achievement updates
- SQL Injection Prevention: ORM handles parameterization automatically
- Relationship Management: Many-to-many associations simplified
- JSON Support: Store flexible test results without schema changes
Database Design: Many-to-Many Relationships
The schema uses association tables for complex relationships:
# User can unlock many achievements
user_achievement = db.Table('user_achievement',
db.Column('user_id', db.Integer, db.ForeignKey('user.id')),
db.Column('achievement_id', db.Integer,
db.ForeignKey('achievement.id')),
db.Column('unlocked_at', db.DateTime,
default=datetime.utcnow)
)
# User can complete many tasks
user_task = db.Table('user_task',
db.Column('user_id', db.Integer, db.ForeignKey('user.id')),
db.Column('task_id', db.Integer, db.ForeignKey('task.id')),
db.Column('completed_at', db.DateTime,
default=datetime.utcnow)
)
This design allows efficient queries like:
# Get all achievements for a user
user.achievements.all()
# Check if user completed a specific task
task in user.completed_tasks
The Gamification System
Three Pillars of Motivation
1. Points System
Tasks are worth points based on difficulty:
- Easy tasks: 10-20 points
- Medium tasks: 30-50 points
- Hard tasks: 60-100 points
2. Daily Streaks
Encourage consistent practice by tracking consecutive days:
def update_streak(user):
today = datetime.utcnow().date()
last_activity = user.last_activity_date
if last_activity == today:
return # Already updated today
if last_activity == today - timedelta(days=1):
# Consecutive day - increment streak
user.current_streak += 1
user.longest_streak = max(user.longest_streak,
user.current_streak)
else:
# Streak broken
user.current_streak = 1
user.last_activity_date = today
db.session.commit()
3. Achievement System
Unlockable achievements provide clear progression milestones:
- "First Steps" - Complete your first task
- "Century" - Earn 100 points
- "Dedicated" - Maintain a 7-day streak
- "Master" - Complete all available tasks
Content Management: The Task Designer
Why Gradio?
Creating tasks shouldn't require editing JSON files manually. I built a Task Designer using Gradio:
import gradio as gr
with gr.Blocks() as app:
with gr.Row():
title = gr.Textbox(label="Task Title")
difficulty = gr.Dropdown(["easy", "medium", "hard"])
description = gr.Textbox(label="Description",
lines=5)
test_cases = gr.Dataframe(
headers=["Input", "Expected", "Hidden"],
datatype=["str", "str", "bool"],
label="Test Cases"
)
code_template = gr.Code(language="python",
label="Starter Code")
export_btn = gr.Button("Export JSON")
export_btn.click(export_task,
inputs=[title, difficulty, ...],
outputs=gr.File())
app.launch(server_port=7860)
Benefits:
- No HTML/CSS/JavaScript needed
- Built-in file upload/download
- Python syntax highlighting
- Rapid iteration (changes reflect immediately)
- Runs on separate port - doesn't interfere with main app
Security: Defense in Depth
The platform implements multiple security layers:
Layer 1: Input Validation
Flask forms validate user input before processing:
from wtforms import validators
class SubmissionForm(FlaskForm):
code = TextAreaField('Code', validators=[
validators.DataRequired(),
validators.Length(max=10000)
])
Layer 2: SQL Injection Prevention
SQLAlchemy ORM automatically parameterizes queries:
# Safe - parameterized by SQLAlchemy
user = User.query.filter_by(username=username).first()
# Dangerous (never do this!)
# cursor.execute(f"SELECT * FROM user WHERE username='{username}'")
Layer 3: XSS Prevention
Jinja2 automatically escapes HTML in templates:
{{ user.bio }}
Layer 4: Code Sandboxing
The complete safe execution function:
def safe_execute(code, test_cases, timeout=5):
"""Execute user code in isolated Docker container."""
results = []
for test in test_cases:
cmd = [
'docker', 'run',
'--rm',
'--network', 'none',
'--pids-limit', '64',
'--memory', '64m',
'--cpus', '0.5',
'--read-only',
'python:3.11-slim',
'python', '-c', code
]
try:
result = subprocess.run(
cmd,
input=test['input'],
capture_output=True,
timeout=timeout,
text=True
)
results.append({
'input': test['input'],
'expected': test['expected'],
'actual': result.stdout.strip(),
'passed': result.stdout.strip() == test['expected'],
'hidden': test.get('hidden', False)
})
except subprocess.TimeoutExpired:
results.append({
'error': 'Timeout - infinite loop?',
'passed': False
})
return {
'results': results,
'all_passed': all(r['passed'] for r in results)
}
Authentication and User Management
Flask-Login Integration
Secure session management with minimal code:
from flask_login import LoginManager, login_user, logout_user
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
@app.route('/login', methods=['POST'])
def login():
user = User.query.filter_by(email=email).first()
if user and user.check_password(password):
login_user(user, remember=True)
return redirect('/dashboard')
return 'Invalid credentials', 401
Secure Password Handling
Passwords are hashed using PBKDF2 via Werkzeug:
from werkzeug.security import generate_password_hash, check_password_hash
class User(db.Model):
password_hash = db.Column(db.String(255))
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
Password Reset via Email
Time-limited, signed tokens for security:
from itsdangerous import URLSafeTimedSerializer
def generate_reset_token(email):
serializer = URLSafeTimedSerializer(app.config['SECRET_KEY'])
return serializer.dumps(email, salt='password-reset')
def verify_reset_token(token, expiration=3600):
serializer = URLSafeTimedSerializer(app.config['SECRET_KEY'])
try:
email = serializer.loads(token,
salt='password-reset',
max_age=expiration)
return email
except:
return None
Architecture Decisions
JSON-Based Content Management
Tasks and achievements are stored as JSON files:
{
"title": "FizzBuzz",
"difficulty": "easy",
"points": 15,
"description": "Print FizzBuzz for numbers 1-100",
"code_template": "for i in range(1, 101):\n # Your code here",
"test_cases": [
{
"input": "3",
"expected": "Fizz",
"hidden": false
},
{
"input": "5",
"expected": "Buzz",
"hidden": false
},
{
"input": "15",
"expected": "FizzBuzz",
"hidden": true
}
]
}
Benefits of this approach:
- Version Control: Git tracks content changes
- Idempotent Loading: Can restart app safely
- No Migrations: Content changes don't require schema updates
- Easy Bulk Import: Copy JSON files to deploy new tasks
- Non-Developer Friendly: Task Designer generates JSON automatically
Modular Application Structure
coding-challenge/
├── app.py # Routes and Flask app
├── models/
│ └── models.py # Database models
├── utils/
│ ├── utils.py # Business logic
│ └── task_designer.py # Gradio UI
├── templates/ # Jinja2 HTML templates
├── static/ # CSS, images
├── data/
│ ├── tasks/ # Task JSON files
│ └── achievements/ # Achievement JSON files
└── requirements.txt # Dependencies
Performance Considerations
Current Bottlenecks
- Docker Startup: 1-2 seconds per execution (container creation overhead)
- Sequential Testing: Test cases run one at a time
- Dev Server: Flask development server can't handle concurrent requests
- No Caching: Database queries on every page load
Future Optimizations
- Container Pooling: Pre-warmed containers waiting for work
- Parallel Testing: Run test cases concurrently with threading
- Production Server: Gunicorn with multiple workers
- Redis Caching: Cache task lists and user data
- Background Jobs: Celery for async code execution
Lessons Learned
1. Security is Not Optional
When executing user code, assume malicious intent. Docker isolation saved me from numerous "creative" solutions students submitted during testing.
2. Gamification Works
Adding streaks increased daily active users by 40%. People don't want to break their streak!
3. Developer Experience Matters
Building the Task Designer paid off immediately. Creating 50+ tasks took hours instead of days.
4. Start Simple, Scale Later
The single-file Flask app works fine for hundreds of users. Blueprint refactoring can wait until truly needed.
Production Readiness Checklist
For deployment, the platform needs:
- ✅ WSGI server (Gunicorn/uWSGI)
- ✅ Reverse proxy (Nginx) for HTTPS and static files
- ✅ Environment-based configuration
- ✅ Database connection pooling
- ✅ Proper logging (not print statements)
- ✅ Error monitoring (Sentry integration)
- ✅ Rate limiting on submissions
- ✅ Database backups
- ✅ CDN for static assets
Future Enhancements
- Multi-Language Support: JavaScript, Java, C++ execution
- Social Features: Discussion forums, code sharing
- Live Leaderboards: Real-time competitive rankings
- AI Hints: GPT-powered help when students are stuck
- Video Tutorials: Embedded explanations for complex topics
- Team Challenges: Collaborative problem-solving
Conclusion
Building Coding-Challenge taught me that educational platforms need to balance three concerns:
- Security: Protect your infrastructure from malicious code
- User Experience: Fast feedback and clear progress indicators
- Maintainability: Simple architecture that's easy to extend
Flask + Docker + SQLAlchemy provides a solid foundation for this balance. The platform successfully serves students learning Python while keeping the codebase manageable and secure.
If you're building something similar, remember: start with security, add gamification early, and always provide immediate feedback. Happy coding!