Personal CanvasAPI Examples
These are practical Python examples for UCF Open's CanvasAPI library, the canvasapi Python wrapper around Instructure's Canvas LMS API.
CanvasAPI Links
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
Print Course Metadata
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}")