Text-to-QTI Converter
Convert markdown-based quiz files to QTI format for Canvas LMS import.
Overview
Text-to-QTI is a Python CLI tool that converts simple markdown quiz files into QTI (Question and Test Interoperability) packages compatible with Canvas LMS. It supports multiple choice and true/false questions with optional feedback.
Features
- 📝 Simple markdown syntax for quiz authoring
- ✅ Pre-parsing syntax validation with helpful error messages
- 🎯 Support for Multiple Choice and True/False questions
- 📦 Canvas-compatible QTI 1.2 ZIP package generation
- 🔧 Extensible architecture for adding new question types
- 🎨 Colorful CLI with progress indicators
- 📊 Configurable metadata (title, description, points, shuffle)
Installation
From PyPI (Recommended)
pip install text-to-qti
From GitHub
pip install git+https://github.com/tableaprogramming-rgb/textmd-to-qti.git
From Source (Development)
git clone https://github.com/tableaprogramming-rgb/textmd-to-qti.git
cd textmd-to-qti
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
pip install -e .
Requirements
- Python 3.8+
- lxml
- pydantic
- click
- rich
- markdown
- PyYAML
Quick Start
1. Create a Quiz File
Create a file quiz.txt:
---
title: My First Quiz
description: A simple quiz about geography
points_per_question: 1
shuffle_answers: false
---
## Question 1
[Type: multiple_choice]
What is the capital of France?
a) London
b) Berlin
*c) Paris
d) Madrid
Feedback: Paris is the capital and largest city of France.
## Question 2
[Type: true_false]
The Earth is flat.
*a) False
b) True
Feedback: The Earth is an oblate spheroid.
2. Validate (Optional)
text-to-qti validate quiz.txt
3. Convert to QTI
text-to-qti convert quiz.txt -o quiz.zip
4. Import to Canvas
- Go to your Canvas course
- Navigate to Quizzes
- Click Import Existing Content
- Select QTI .zip file
- Upload
quiz.zip
Syntax Guide
File Structure
Every quiz file has:
- Optional YAML front matter with metadata
- Section headers (optional) for organization
- Questions with metadata and answer choices
YAML Front Matter
---
title: Quiz Title (required)
description: Optional description
points_per_question: 1 (default)
shuffle_answers: false (default)
---
Question Format
## Question N
[Type: question_type]
[Points: number]
[ID: custom_id]
Question text here.
a) Answer choice 1
b) Answer choice 2
*c) Correct answer
d) Answer choice 4
Feedback: Optional feedback shown after answering.
Metadata Tags
[Type: multiple_choice]or[Type: true_false](required)[Points: number]- Points for this question (default: from metadata)[ID: custom_id]- Custom question ID (default: auto-generated)Feedback:- Feedback text shown after answering (optional)
Answer Choices
- Each choice starts with a letter:
a),b),c), etc. - Letters must be sequential
- Exactly one answer must be marked as correct with
**a) Correct answerb) Wrong answer
- For True/False: must have exactly 2 choices
Text Formatting
Question text supports markdown:
## Question 1
[Type: multiple_choice]
This is **bold** text and *italic* text.
You can use `inline code` or [links](https://example.com).
a) First choice
*b) Correct choice
Comments
HTML comments are ignored:
<!-- This comment will be ignored -->
## Question 1
[Type: multiple_choice]
CLI Commands
Convert Command
text-to-qti convert INPUT_FILE [OPTIONS]
Options:
-o, --output PATH Output ZIP file path (default: output.zip)
--validate-only Only validate syntax, don't generate
--qti-version {1.2,2.1} QTI version (default: 1.2)
Validate Command
text-to-qti validate INPUT_FILE
Validates syntax without generating QTI.
Examples
Simple Multiple Choice
---
title: Basic Math Quiz
---
## Question 1
[Type: multiple_choice]
What is 2 + 2?
a) 3
*b) 4
c) 5
True/False with Custom Points
---
title: Science Quiz
points_per_question: 2
---
## Question 1
[Type: true_false]
[Points: 5]
Photosynthesis requires sunlight.
*a) True
b) False
Feedback: Photosynthesis is the process plants use to convert light energy into chemical energy.
Mixed Question Types
---
title: Comprehensive Quiz
description: Tests various knowledge areas
points_per_question: 1
---
# Section 1: Multiple Choice
## Question 1
[Type: multiple_choice]
What programming language is known for web development?
a) Python
b) Go
*c) JavaScript
d) Rust
## Question 2
[Type: multiple_choice]
Which is NOT a relational database?
a) PostgreSQL
b) MySQL
c) Oracle
*d) MongoDB
# Section 2: True/False
## Question 3
[Type: true_false]
Git is a distributed version control system.
*a) True
b) False
## Question 4
[Type: true_false]
REST APIs always use JSON.
a) True
*b) False
Error Messages
The validator provides helpful error messages:
ERROR: Line 15: Question 1: No correct answer specified.
Mark correct answer with * (e.g., *c) Correct answer)
ERROR: Line 20: Question 2: Answer letters must be sequential (a, b, c, ...).
Found: a, c
ERROR: Line 25: Question 3: True/False questions must have exactly 2 choices, found 3
Canvas Import Guide
- Prepare your quiz file - Use the syntax guide above
- Validate - Run
text-to-qti validate quiz.txtto check for errors - Convert - Run
text-to-qti convert quiz.txtto createoutput.zip - Import to Canvas:
- Go to your course
- Click Settings → Import Existing Content
- Select QTI .zip file
- Upload the ZIP file
- Review the imported quiz in your question bank or quizzes
Architecture
The project is designed with extensibility in mind:
Input Text File
↓
[Parser] ──→ Validates & parses markdown
↓
[Question Models] ──→ Pydantic validated data
↓
[Item Generators] ──→ Pluggable generators per question type
↓
[QTI XML Builders] ──→ Creates assessment & manifest
↓
[ZIP Packager] ──→ Creates Canvas-ready package
↓
Output QTI ZIP
Adding New Question Types
To add a new question type:
- Create a new generator class inheriting from
BaseItemGenerator - Implement the
generate()method - Register with the main generator
Example (in your code):
from text_to_qti.qti.base_item import BaseItemGenerator
from text_to_qti.qti.generator import QTIGenerator
class FillInBlankGenerator(BaseItemGenerator):
def generate(self, question):
# Custom implementation
pass
# Use it
generator = QTIGenerator(quiz)
generator.register_item_generator(
QuestionType.FILL_IN_BLANK,
FillInBlankGenerator()
)
Development
Install Development Dependencies
pip install -e ".[dev]"
Run Tests
pytest # Run all tests
pytest -v # Verbose output
pytest --cov # With coverage report
Code Quality
black src/ # Format code
ruff check src/ # Lint
mypy src/ # Type checking
Limitations
Current MVP limitations (Phase 1):
- Only supports Multiple Choice and True/False questions
- QTI 1.2 format only (2.1 support coming)
- No image/media support yet
- No question shuffling within quiz (only answer shuffling)
- No partial credit support
- No essay/free-form response questions
Troubleshooting
"File must be UTF-8 encoded"
Save your quiz file as UTF-8. In most editors:
- VS Code: Click encoding in bottom right, select UTF-8
- Sublime: File → Save with Encoding → UTF-8
- macOS: Use TextEdit → Format → Plain Text, then save
"No questions found"
Ensure questions start with ## Question N where N is a number.
"Invalid question type"
Ensure you use exactly: [Type: multiple_choice] or [Type: true_false]
"Answer letters must be sequential"
Answers must be a, b, c, d, etc. in order. You can't skip letters.
Contributing
Contributions are welcome and appreciated! Please see CONTRIBUTING.md for:
- Development setup instructions
- How to run tests locally
- Code style guidelines
- How to submit pull requests
- Reporting bugs and suggesting features
Acknowledgments
This project was made possible with Claude, an AI assistant by Anthropic. Claude contributed significantly to:
- Architecture Design - Conceptualized the overall system design for QTI generation
- Implementation - Built the complete markdown-to-QTI conversion pipeline
- Testing - Developed comprehensive test suite (57 tests, 95%+ coverage)
- Documentation - Created all guides, templates, and setup documentation
- DevOps - Configured GitHub Actions CI/CD and PyPI publishing automation
- Best Practices - Implemented repository standards and security scanning
We're grateful for Claude's essential contributions to making educational technology more accessible.
License
MIT License - See LICENSE file for details
Support
For issues, questions, or suggestions:
- Check the troubleshooting section
- Review examples for similar use cases
- Check existing issues on GitHub
- Create a new issue with details
Changelog
See CHANGELOG.md for the full version history and release notes.
Latest Version: 0.1.0 (Initial MVP)
- ✅ Markdown parser with YAML metadata
- ✅ Multiple choice and true/false support
- ✅ Syntax validation with error reporting
- ✅ QTI 1.2 package generation
- ✅ Canvas LMS compatibility
- ✅ CLI with progress indicators
- ✅ Extensible architecture for question types
Roadmap
Phase 2 (Coming Soon)
- Fill-in-the-blank questions
- Multiple answer questions
- Image/media support
- QTI 2.1 format support
- Quiz import/editing from Canvas
Phase 3 (Future)
- Web UI for quiz creation
- Question bank management
- CSV/Excel import
- Question randomization
- Partial credit support
Made with ❤️ for educators
Metadata
Release files for text-to-qti 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| text_to_qti-0.1.1.tar.gz | 37.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| text_to_qti-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 68.6 kB
Release files / text_to_qti-0.1.1.tar.gz
| Download URL | text_to_qti-0.1.1.tar.gz |
|---|---|
| Size | 37.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b9ee27dcc719bde0eddef3e7788f665644f66765cc9d537f23bfad0f02923dd7
|
|
BLAKE2b-256 checksum How to use checksums |
432893ae67006a5210e4cc88eaa0148ecbedf1fbea04289e934b418329aa8b42
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.14
|
Release files / text_to_qti-0.1.1-py3-none-any.whl
| Download URL | text_to_qti-0.1.1-py3-none-any.whl |
|---|---|
| Size | 30.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fb042852af8b0dcfb35ff3971aac98aa49e1bf3d23abff3ea8d6829d4bf05104
|
|
BLAKE2b-256 checksum How to use checksums |
e45096a6ac7c115fd24a4eba40588266463e8e8e8976ae1da228f220bdac4906
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.14
|