Personal CanvasAPI Examples

These are practical Python examples for UCF Open's CanvasAPI library, the canvasapi Python wrapper around Instructure's Canvas LMS API.

Project: GitHub repository, PyPI package, and Read the Docs.

Official CanvasAPI docs: Getting Started, Examples, and Class Reference.

Boilerplate

All examples assume a canvas and course object like this:

from canvasapi import Canvas

API_URL = "https://canvas.example.edu"
API_KEY = "CANVAS_API_TOKEN"
COURSE_ID = 123456

canvas = Canvas(API_URL, API_KEY)
course = canvas.get_course(COURSE_ID)

Or, when using a local keychain entry:

import keyring
from canvasapi import Canvas

API_URL = "https://canvas.example.edu"
API_KEY = keyring.get_password("KEYCHAIN_APP_NAME", "KEYCHAIN_ACCOUNT_NAME")
COURSE_ID = 123456

canvas = Canvas(API_URL, API_KEY)
course = canvas.get_course(COURSE_ID)

Courses And Users

term = getattr(course, "term", {})

print(course.name)
print(course.id)
print(term.get("name"))
print(course.workflow_state)

List Sections

sections = course.get_sections()

for section in sections:
	print(section.id, section.name)

Get Students

students = course.get_users(enrollment_type = ["student"])

for student in students:
	print(student.id, student.name)

Get Teachers, TAs, And Designers

users = course.get_users(enrollment_type = ["teacher", "ta", "designer"])

for user in users:
	print(user.id, user.name)

Files And Folders

Find A Folder By Name

folder = None

for candidate in course.get_folders():
	if candidate.name == "FOLDER_NAME":
		folder = candidate
		break

if folder is None:
	raise RuntimeError("Folder not found")

Create A Folder Under Course Files

course_files = None

for folder in course.get_folders():
	if folder.name == "course files":
		course_files = folder
		break

if course_files is None:
	raise RuntimeError("Course files folder not found")

new_folder = course_files.create_folder("FOLDER_NAME")

Upload A File

from pathlib import Path

folder = None

for candidate in course.get_folders():
	if candidate.name == "FOLDER_NAME":
		folder = candidate
		break

if folder is None:
	raise RuntimeError("Folder not found")

upload_path = Path("local-file.pdf")
upload_ok, upload_info = folder.upload(upload_path)

if upload_ok:
	remote_file_id = upload_info["id"]
	print(remote_file_id)

Upload Only If A File Is Missing

display_name = "local-file.pdf"
existing = None

for file in folder.get_files():
	if file.display_name == display_name:
		existing = file
		break

if existing is None:
	folder.upload(display_name)
else:
	print(f"Already uploaded: {existing.display_name}")

Rename A File

CanvasAPI does not expose every file update as a convenience method. Use the file requester for the direct Canvas endpoint.

for file in folder.get_files():
	new_name = "PREFIX - " + file.display_name
	file._requester.request("PUT", f"files/{file.id}", name = new_name)

Replace A File With The Same Name

Use on_duplicate = "overwrite" when the new local file should replace the existing Canvas file with the same name in the same folder. Use on_duplicate = "rename" when Canvas should keep both files.

upload_ok, upload_info = folder.upload(
	"local-file.pdf",
	on_duplicate = "overwrite",
)

if upload_ok:
	print(upload_info["id"])

Delete A File Only When You Really Mean It

Deleting first is for cleanup or repair, not the usual way to update file contents. Anything that links to the old Canvas file ID may need to be updated.

CONFIRM = False
FILE_ID = 123456

old_file = canvas.get_file(FILE_ID)

if CONFIRM:
	old_file.delete()
else:
	print(f"Would delete file {FILE_ID}")

Modules And Pages

Find Or Create A Module

module = None

for candidate in course.get_modules():
	if candidate.name == "MODULE_NAME":
		module = candidate
		break

if module is None:
	module = course.create_module(module = {"name": "MODULE_NAME"})

Add A File To A Module

module.create_module_item({
	"title": "Module item title",
	"type": "File",
	"content_id": remote_file_id,
	"published": True,
})

Add A Module Item At A Specific Position

Canvas module item positions are 1-based. This inserts the new item at position 2 and shifts later items down.

module.create_module_item({
	"title": "Module item title",
	"type": "File",
	"content_id": remote_file_id,
	"position": 2,
	"published": True,
})

Add An External URL To A Module

module.create_module_item({
	"title": "External resource",
	"type": "ExternalUrl",
	"external_url": "https://example.edu/resource",
	"new_tab": True,
	"published": True,
})

Move An Existing Module Item

module_item = module.get_module_item(MODULE_ITEM_ID)
module_item.edit(module_item = {"position": 2})

Publish All Modules

for module in course.get_modules():
	module.edit(module = {"published": True})

Get The Front Page

front_page = None

for page in course.get_pages():
	if page.front_page:
		front_page = page
		break

Create Or Update A Front Page

wiki_page = {
	"title": "Home",
	"body": "<p>Welcome to the course.</p>",
	"front_page": True,
	"published": True,
}

if front_page is None:
	course.create_page(wiki_page = wiki_page)
else:
	front_page.edit(wiki_page = wiki_page)

Announcements And Discussions

List Announcements

announcements = course.get_discussion_topics(only_announcements = True)

for announcement in announcements:
	print(announcement.id, announcement.title)

Find An Announcement By Title

announcement = None

for candidate in course.get_discussion_topics(only_announcements = True):
	if candidate.title == "ANNOUNCEMENT_TITLE":
		announcement = candidate
		break

Create A Delayed Announcement

course.create_discussion_topic(
	title = "ANNOUNCEMENT_TITLE",
	message = "<p>Announcement body.</p>",
	is_announcement = True,
	delayed_post_at = "2026-08-24T15:00:00Z",
	published = True,
)

Update An Announcement

announcement.update(
	title = "ANNOUNCEMENT_TITLE",
	message = "<p>Updated body.</p>",
	delayed_post_at = "2026-08-24T15:00:00Z",
)

Delete Announcements With A Guard

CONFIRM = False

for announcement in course.get_discussion_topics(only_announcements = True):
	if CONFIRM:
		announcement.delete()
	else:
		print(f"Would delete announcement: {announcement.title}")

Delete Discussions With A Guard

CONFIRM = False

for discussion in course.get_discussion_topics():
	if getattr(discussion, "is_announcement", False):
		continue
	if CONFIRM:
		discussion.delete()
	else:
		print(f"Would delete discussion: {discussion.title}")

Assignments

Find An Assignment Group

assignment_group = None

for group in course.get_assignment_groups():
	if group.name == "ASSIGNMENT_GROUP_NAME":
		assignment_group = group
		break

if assignment_group is None:
	raise RuntimeError("Assignment group not found")

Create An Assignment Group

assignment_group = course.create_assignment_group(name = "ASSIGNMENT_GROUP_NAME")

Set Drop Rules On An Assignment Group

assignment_group.edit(rules = {"drop_lowest": 2})

Create A Basic Assignment

assignment = course.create_assignment({
	"name": "ASSIGNMENT_NAME",
	"assignment_group_id": assignment_group.id,
	"points_possible": 10,
	"submission_types": ["online_upload"],
	"published": False,
})

Update Assignment Dates

assignment.edit(assignment = {
	"due_at": "2026-09-01T06:59:00Z",
	"unlock_at": "2026-08-25T15:00:00Z",
	"lock_at": "2026-09-02T06:59:00Z",
})

Clear Lock Dates For Assignments In A Group

CONFIRM = False

for assignment in course.get_assignments():
	if assignment.assignment_group_id != assignment_group.id:
		continue
	if CONFIRM:
		assignment.edit(assignment = {
			"unlock_at": assignment.due_at,
			"lock_at": None,
		})
	else:
		print(f"Would clear lock date: {assignment.name}")

Create Section-Specific Assignment Overrides

from datetime import timedelta
from dateutil import parser

for section in course.get_sections():
	if not section.name.startswith("SECTION_PREFIX"):
		continue

	base_due = parser.isoparse(assignment.due_at)
	new_due = (base_due + timedelta(hours = 24)).isoformat()

	assignment.create_override(assignment_override = {
		"course_section_id": section.id,
		"due_at": new_due,
	})

Clear Assignment Overrides

CONFIRM = False

for override in assignment.get_overrides():
	if CONFIRM:
		override.delete()
	else:
		print(f"Would delete override {override.id}")

External Tools

Create An External-Tool Assignment

assignment = course.create_assignment({
	"name": "EXTERNAL_TOOL_ASSIGNMENT",
	"published": False,
	"submission_types": ["external_tool"],
	"external_tool_tag_attributes": {
		"url": "https://external-tool.example.edu/launch",
		"content_id": CONTENT_ID,
		"new_tab": True,
	},
})

Inspect External Tool Attributes

assignment = course.get_assignment(ASSIGNMENT_ID)
attrs = getattr(assignment, "external_tool_tag_attributes", None)

print(assignment.name)
print(assignment.submission_types)

if attrs:
	print(attrs.get("content_id"))
	print(attrs.get("tool_id"))
	print(attrs.get("context_external_tool_id"))
	print(attrs.get("resource_link_id"))
	print(attrs.get("url"))
	print(attrs.get("new_tab"))

Reattach Or Repair An External Tool

assignment.edit(assignment = {
	"submission_types": ["external_tool"],
	"external_tool_tag_attributes": {
		"url": "https://external-tool.example.edu/launch",
		"content_id": CONTENT_ID,
		"new_tab": True,
	},
})

Update External Tool Content IDs In A Group

CONFIRM = False

for assignment in course.get_assignments():
	if assignment.assignment_group_id != assignment_group.id:
		continue
	if "external_tool" not in getattr(assignment, "submission_types", []):
		continue

	attrs = getattr(assignment, "external_tool_tag_attributes", None)
	if not attrs:
		continue

	if CONFIRM:
		assignment.edit(assignment = {
			"external_tool_tag_attributes": {
				"context_external_tool_id": CONTENT_ID,
			},
		})
	else:
		print(f"Would update content id for {assignment.name}")

Remove An External Tool From An Assignment

CONFIRM = False

if CONFIRM:
	assignment.edit(assignment = {
		"submission_types": ["online_upload"],
		"external_tool_tag_attributes": None,
	})
else:
	print(f"Would remove external tool from {assignment.name}")

Submissions And Grades

Update One Submission Grade

submission = assignment.get_submission(USER_ID)
submission.edit(submission = {"posted_grade": 9.5})

Add A Text Comment To A Submission

CanvasAPI sends submission comments through the comment keyword. Submission comments are additive in Canvas: calling this once creates one comment, and calling it again creates another comment. It does not replace the previous comment.

submission = assignment.get_submission(USER_ID)
submission.edit(comment = {"text_comment": "Thanks for uploading the file."})

Update Grades And Comments From Local Results

results = [
	{"user_id": USER_ID, "score": 10, "comment": "Good work."},
]

for result in results:
	submission = assignment.get_submission(result["user_id"])
	submission.edit(submission = {"posted_grade": result["score"]})
	submission.edit(comment = {"text_comment": result["comment"]})

Merge Scores From Two Assignments

first_assignment = course.get_assignment(FIRST_ASSIGNMENT_ID)
second_assignment = course.get_assignment(SECOND_ASSIGNMENT_ID)
target_assignment = course.get_assignment(TARGET_ASSIGNMENT_ID)

for user in course.get_users(enrollment_type = ["student"]):
	first_grade = first_assignment.get_submission(user.id).grade or 0
	second_grade = second_assignment.get_submission(user.id).grade or 0
	new_score = float(first_grade) + float(second_grade)

	target_submission = target_assignment.get_submission(user.id)
	target_submission.edit(submission = {"posted_grade": new_score})

Read Submission Comments

submission_path = (
	f"courses/{course.id}/assignments/{assignment.id}/submissions/{USER_ID}"
)

response = assignment._requester.request(
	"GET",
	submission_path + "?include=submission_comments",
)
submission_json = response.json()

for comment in submission_json["submission_comments"]:
	print(comment["id"], comment.get("comment"))

Delete Submission Comments With A Guard

Use this when you need to clean up duplicate or stale comments. Deleting comments is separate from posting a new comment.

CONFIRM = False

submission_path = (
	f"courses/{course.id}/assignments/{assignment.id}/submissions/{USER_ID}"
)
response = assignment._requester.request(
	"GET",
	submission_path + "?include=submission_comments",
)

for comment in response.json()["submission_comments"]:
	comment_path = submission_path + f"/comments/{comment['id']}"
	if CONFIRM:
		assignment._requester.request("DELETE", comment_path)
	else:
		print(f"Would delete comment {comment['id']}")

Bulk Grade Updates

Use the bulk grade endpoint when you need to upload many grades for the same assignment or for many assignments from a gradebook export. It is much faster than looping over submissions and calling submission.edit(...) one student at a time because Canvas accepts a batch of grade changes and processes it as an asynchronous job.

Parse Assignment IDs From Gradebook Headers

import csv
import re

def assignment_id_from_header(header):
	match = re.search(r"\((\d+)\)", header)
	if match:
		return int(match.group(1))
	return None

updates = {}

with open("gradebook.tsv", newline = "") as gradebook:
	reader = csv.reader(gradebook, delimiter = "\t")
	headers = next(reader)
	assignment_columns = [
		(index, assignment_id_from_header(header))
		for index, header in enumerate(headers)
		if assignment_id_from_header(header) is not None
	]

	for row in reader:
		user_id = row[1].strip()
		if not user_id or row[0].strip() == "Points Possible":
			continue

		for index, assignment_id in assignment_columns:
			score = row[index].strip()
			if not score:
				continue
			updates.setdefault(assignment_id, {})[int(user_id)] = score

Upload Grades Through The Direct update_grades Endpoint

Canvas returns a progress URL for bulk grade updates, so poll it before moving on.

import time
import requests

headers = {"Authorization": f"Bearer {API_KEY}"}
assignment_id = ASSIGNMENT_ID
grade_map = {
	USER_ID: 10,
}

payload = {}
for user_id, score in grade_map.items():
	payload[f"grade_data[{user_id}][posted_grade]"] = score

url = (
	f"{API_URL}/api/v1/courses/{COURSE_ID}/assignments/"
	f"{assignment_id}/submissions/update_grades"
)

response = requests.post(url, headers = headers, data = payload, timeout = 30)
response.raise_for_status()

progress_url = response.json().get("progress_url")

while progress_url:
	progress = requests.get(progress_url, headers = headers, timeout = 30)
	progress.raise_for_status()
	status = progress.json()
	if status["workflow_state"] == "completed":
		break
	if status["workflow_state"] == "failed":
		raise RuntimeError("Canvas grade update failed")
	time.sleep(1)

Conversations

Mark All Canvas Messages Read

CONFIRM = False

before = canvas.conversations_unread_count().get("unread_count", 0)
print(f"Unread before: {before}")

if CONFIRM:
	ok = canvas.conversations_mark_all_as_read()
	if not ok:
		raise RuntimeError("Canvas did not confirm success")
	after = canvas.conversations_unread_count().get("unread_count", 0)
	print(f"Unread after: {after}")
else:
	print("Would mark all Canvas conversations as read")

Guarded Admin Reset Recipes

These examples are intentionally guarded. Leave CONFIRM = False until you are sure you are pointing at the right course.

Canvas courses usually have a browser-only restore page at:

https://canvas.example.edu/courses/COURSE_ID/undelete

Use that page to look for recently deleted content after an accidental delete. It is a recovery aid, not a backup: not every deleted item is guaranteed to appear there, and restored items should still be checked in Canvas.

If assignment grades disappear because an assignment was deleted, try undeleting the assignment. Restoring the assignment can restore the associated gradebook column and grades, but verify the gradebook afterward.

Check Whether Students May Have Access

Run this before changing CONFIRM to True. A published course has workflow_state == "available". If the course also restricts access to course dates, check start_at and end_at too.

from datetime import datetime, timezone

def parse_canvas_time(value):
	if not value:
		return None
	if isinstance(value, datetime):
		return value
	return datetime.fromisoformat(value.replace("Z", "+00:00"))

def students_may_have_access(course):
	if course.workflow_state != "available":
		return False

	if not getattr(course, "restrict_enrollments_to_course_dates", False):
		return True

	now = datetime.now(timezone.utc)
	start_at = parse_canvas_time(getattr(course, "start_at", None))
	end_at = parse_canvas_time(getattr(course, "end_at", None))

	if start_at and now < start_at:
		return False
	if end_at and now > end_at:
		return False
	return True

if students_may_have_access(course):
	raise RuntimeError(f"Students may have access to {course.name}. Stop.")

Delete Modules

CONFIRM = False

for module in course.get_modules():
	if CONFIRM:
		module.delete()
	else:
		print(f"Would delete module: {module.name}")

Delete Assignments And Assignment Groups

CONFIRM = False

for assignment in course.get_assignments():
	if CONFIRM:
		assignment.delete()
	else:
		print(f"Would delete assignment: {assignment.name}")

for group in course.get_assignment_groups():
	if CONFIRM:
		group.delete()
	else:
		print(f"Would delete assignment group: {group.name}")

Delete Files And Folders

CONFIRM = False

for file in course.get_files():
	if CONFIRM:
		file.delete()
	else:
		print(f"Would delete file: {file.display_name}")

for folder in course.get_folders():
	if folder.parent_folder_id is None:
		continue
	if CONFIRM:
		folder.delete(force = True)
	else:
		print(f"Would delete folder: {folder.name}")

Delete Pages

CONFIRM = False

if CONFIRM:
	course.create_page(wiki_page = {
		"title": "Blank Page",
		"front_page": True,
		"published": True,
	})

for page in course.get_pages():
	if page.front_page:
		continue
	if CONFIRM:
		page.delete()
	else:
		print(f"Would delete page: {page.title}")

Delete Quizzes, Groups, And Calendar Events

CONFIRM = False

for quiz in course.get_quizzes():
	if CONFIRM:
		quiz.delete()
	else:
		print(f"Would delete quiz: {quiz.title}")

for group_category in course.get_group_categories():
	if CONFIRM:
		group_category.delete()
	else:
		print(f"Would delete group category: {group_category.name}")

context_code = f"course_{course.id}"
events = canvas.get_calendar_events(
	all_events = True,
	context_codes = [context_code],
)

for event in events:
	if CONFIRM:
		event.delete()
	else:
		print(f"Would delete calendar event: {event.title}")

Reset Syllabus Body

CONFIRM = False

if CONFIRM:
	course.update_settings(syllabus_course_summary = True)
	course.update(course = {"syllabus_body": ""})
else:
	print("Would clear syllabus body")

Reset Course Navigation

CONFIRM = False
visible_tabs = [
	"Home",
	"Announcements",
	"Assignments",
	"Discussions",
	"Grades",
	"People",
	"Pages",
	"Files",
	"Syllabus",
	"Quizzes",
	"Modules",
	"Settings",
]

for tab in course.get_tabs():
	hidden = tab.label not in visible_tabs
	position = visible_tabs.index(tab.label) + 1 if tab.label in visible_tabs else None

	if CONFIRM:
		tab.update(hidden = hidden)
		if position is not None:
			tab.update(position = position)
	else:
		print(f"Would update tab: {tab.label}")