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:

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:

Technology Stack Deep Dive

Flask: The Perfect Framework

I chose Flask 3.1.0+ over Django for several reasons:

# 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:

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:

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:

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:

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:

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

  1. Docker Startup: 1-2 seconds per execution (container creation overhead)
  2. Sequential Testing: Test cases run one at a time
  3. Dev Server: Flask development server can't handle concurrent requests
  4. No Caching: Database queries on every page load

Future Optimizations

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:

Future Enhancements

Conclusion

Building Coding-Challenge taught me that educational platforms need to balance three concerns:

  1. Security: Protect your infrastructure from malicious code
  2. User Experience: Fast feedback and clear progress indicators
  3. 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!