diff --git a/CHANGELOG.md b/CHANGELOG.md
index 241ef3e..e489ae0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@
 ### New Endpoint Coverage
 
 - Get all quiz submissions (Thanks, [@petarGitNik](https://github.com/petarGitNik))
+- Upload a file to a submission (Thanks, [@MarkLalor](https://github.com/MarkLalor))
 
 ### General
 
diff --git a/canvasapi/assignment.py b/canvasapi/assignment.py
index 03d1b56..35e1bb8 100644
--- a/canvasapi/assignment.py
+++ b/canvasapi/assignment.py
@@ -3,10 +3,12 @@ from __future__ import absolute_import, division, print_function, unicode_litera
 from six import python_2_unicode_compatible
 
 from canvasapi.canvas_object import CanvasObject
-from canvasapi.exceptions import RequiredFieldMissing
+from canvasapi.exceptions import CanvasException, RequiredFieldMissing
 from canvasapi.paginated_list import PaginatedList
 from canvasapi.progress import Progress
 from canvasapi.submission import Submission
+from canvasapi.upload import Uploader
+from canvasapi.user import User
 from canvasapi.user import UserDisplay
 from canvasapi.util import combine_kwargs, obj_or_id
 
@@ -83,8 +85,6 @@ class Assignment(CanvasObject):
 
         :rtype: :class:`canvasapi.submission.Submission`
         """
-        from canvasapi.user import User
-
         user_id = obj_or_id(user, "user", (User,))
 
         response = self._requester.request(
@@ -116,7 +116,7 @@ class Assignment(CanvasObject):
             _kwargs=combine_kwargs(**kwargs)
         )
 
-    def submit(self, submission, **kwargs):
+    def submit(self, submission, file=None, **kwargs):
         """
         Makes a submission for an assignment.
 
@@ -125,6 +125,9 @@ class Assignment(CanvasObject):
 
         :param submission: The attributes of the submission.
         :type submission: dict
+        :param file: A file to upload with the submission. (Optional,
+            defaults to `None`. Submission type must be `online_upload`)
+        :type file: file or str
 
         :rtype: :class:`canvasapi.submission.Submission`
         """
@@ -135,6 +138,18 @@ class Assignment(CanvasObject):
                 "Dictionary with key 'submission_type' is required."
             )
 
+        if file:
+            if submission.get('submission_type') != 'online_upload':
+                raise ValueError(
+                    "To upload a file, `submission['submission_type']` must be `online_upload`."
+                )
+
+            upload_response = self.upload_to_submission(file, **kwargs)
+            if upload_response[0]:
+                kwargs['submission']['file_ids'] = [upload_response[1]['id']]
+            else:
+                raise CanvasException('File upload failed. Not submitting.')
+
         response = self._requester.request(
             'POST',
             'courses/{}/assignments/{}/submissions'.format(self.course_id, self.id),
@@ -166,6 +181,37 @@ class Assignment(CanvasObject):
         )
         return Progress(self._requester, response.json())
 
+    def upload_to_submission(self, file, user='self', **kwargs):
+        """
+        Upload a file to a submission.
+
+        :calls: `POST /api/v1/courses/:course_id/assignments/:assignment_id/ \
+            submissions/:user_id/files \
+        <https://canvas.instructure.com/doc/api/submissions.html#method.submissions_api.create_file>`_
+
+        :param file: The file or path of the file to upload.
+        :type file: file or str
+        :param user: The object or ID of the related user, or 'self' for the
+            current user. Defaults to 'self'.
+        :type user: :class:`canvasapi.user.User`, int, or str
+
+        :returns: True if the file uploaded successfully, False otherwise, \
+                    and the JSON response from the API.
+        :rtype: tuple
+        """
+        user_id = obj_or_id(user, "user", (User,))
+
+        return Uploader(
+            self._requester,
+            'courses/{}/assignments/{}/submissions/{}/files'.format(
+                self.course_id,
+                self.id,
+                user_id
+            ),
+            file,
+            **kwargs
+        ).start()
+
 
 @python_2_unicode_compatible
 class AssignmentGroup(CanvasObject):
diff --git a/canvasapi/upload.py b/canvasapi/upload.py
index 2d4b4f0..d795764 100644
--- a/canvasapi/upload.py
+++ b/canvasapi/upload.py
@@ -100,7 +100,4 @@ class Uploader(object):
         # remove `while(1);` that may appear at the top of a response
         response_json = json.loads(response.text.lstrip('while(1);'))
 
-        if 'url' in response_json:
-            return (True, response_json)
-
-        return (False, response_json)
+        return ('url' in response_json, response_json)
diff --git a/canvasapi/util.py b/canvasapi/util.py
index e7165b0..0431db8 100644
--- a/canvasapi/util.py
+++ b/canvasapi/util.py
@@ -109,9 +109,15 @@ def obj_or_id(parameter, param_name, object_types):
     :param object_types: tuple
     :rtype: int
     """
+    from canvasapi.user import User
+
     try:
         return int(parameter)
     except (ValueError, TypeError):
+        # Special case where 'self' is a valid ID of a User object
+        if User in object_types and parameter == 'self':
+            return parameter
+
         for obj_type in object_types:
             if isinstance(parameter, obj_type):
                 try:

Add coverage for uploading a file to a submission for an assignment.
Currently, submitting in the context of an assignment is not covered. This endpoint is documented [here](https://canvas.instructure.com/doc/api/submissions.html#method.submissions_api.create_file)

This is really useful to use as a student.
diff --git a/tests/fixtures/assignment.json b/tests/fixtures/assignment.json
index 0e2f6ba..9103ae8 100644
--- a/tests/fixtures/assignment.json
+++ b/tests/fixtures/assignment.json
@@ -101,5 +101,42 @@
 			"url": "https://canvas.example.edu/api/v1/progress/3"
 		},
 		"status_code": 200
+	},
+	"upload": {
+		"method": "POST",
+		"endpoint": "courses/1/assignments/1/submissions/self/files",
+		"data": {
+			"upload_url": "https://example.com/api/v1/files/upload_response_upload_url",
+			"upload_params": {
+				"some_param": "param123",
+				"a_different_param": "param456"
+			}
+		}
+	},
+	"upload_by_id": {
+		"method": "POST",
+		"endpoint": "courses/1/assignments/1/submissions/1/files",
+		"data": {
+			"upload_url": "https://example.com/api/v1/files/upload_response_upload_url",
+			"upload_params": {
+				"some_param": "param123",
+				"a_different_param": "param456"
+			}
+		}
+	},
+	"upload_final": {
+		"method": "POST",
+		"endpoint": "files/upload_response_upload_url",
+		"data": {
+			"url": "great_url_success",
+			"id": 1
+		}
+	},
+	"upload_fail": {
+		"method": "POST",
+		"endpoint": "files/upload_response_upload_url",
+		"data": {
+			"no_url": "bad_url_failure"
+		}
 	}
 }
diff --git a/tests/test_assignment.py b/tests/test_assignment.py
index e5fec7c..f737087 100644
--- a/tests/test_assignment.py
+++ b/tests/test_assignment.py
@@ -1,16 +1,17 @@
 from __future__ import absolute_import, division, print_function, unicode_literals
 import unittest
+import uuid
 
 import requests_mock
 
 from canvasapi import Canvas
 from canvasapi.assignment import Assignment, AssignmentGroup
-from canvasapi.exceptions import RequiredFieldMissing
+from canvasapi.exceptions import CanvasException, RequiredFieldMissing
 from canvasapi.progress import Progress
 from canvasapi.submission import Submission
 from canvasapi.user import UserDisplay
 from tests import settings
-from tests.util import register_uris
+from tests.util import register_uris, cleanup_file
 
 
 @requests_mock.Mocker()
@@ -97,6 +98,46 @@ class TestAssignment(unittest.TestCase):
         with self.assertRaises(RequiredFieldMissing):
             self.assignment.submit({})
 
+    def test_submit_file(self, m):
+        register_uris({'assignment': ['submit', 'upload', 'upload_final']}, m)
+
+        filename = 'testfile_assignment_{}'.format(uuid.uuid4().hex)
+
+        try:
+            with open(filename, 'w+') as file:
+                sub_type = "online_upload"
+                sub_dict = {'submission_type': sub_type}
+                submission = self.assignment.submit(sub_dict, file)
+
+            self.assertIsInstance(submission, Submission)
+            self.assertTrue(hasattr(submission, 'submission_type'))
+            self.assertEqual(submission.submission_type, sub_type)
+
+        finally:
+            cleanup_file(filename)
+
+    def test_submit_file_wrong_type(self, m):
+        filename = 'testfile_assignment_{}'.format(uuid.uuid4().hex)
+        sub_type = "online_text_entry"
+        sub_dict = {'submission_type': sub_type}
+
+        with self.assertRaises(ValueError):
+            self.assignment.submit(sub_dict, filename)
+
+    def test_submit_file_upload_failure(self, m):
+        register_uris({'assignment': ['submit', 'upload', 'upload_fail']}, m)
+
+        filename = 'testfile_assignment_{}'.format(uuid.uuid4().hex)
+
+        try:
+            with open(filename, 'w+') as file:
+                sub_type = "online_upload"
+                sub_dict = {'submission_type': sub_type}
+                with self.assertRaises(CanvasException):
+                    self.assignment.submit(sub_dict, file)
+        finally:
+            cleanup_file(filename)
+
     # __str__()
     def test__str__(self, m):
         string = str(self.assignment)
@@ -119,6 +160,39 @@ class TestAssignment(unittest.TestCase):
         progress = progress.query()
         self.assertTrue(progress.context_type == "Course")
 
+    # upload_to_submission()
+    def test_upload_to_submission_self(self, m):
+        register_uris({'assignment': ['upload', 'upload_final']}, m)
+
+        filename = 'testfile_assignment_{}'.format(uuid.uuid4().hex)
+
+        try:
+            with open(filename, 'w+') as file:
+                response = self.assignment.upload_to_submission(file)
+
+            self.assertTrue(response[0])
+            self.assertIsInstance(response[1], dict)
+            self.assertIn('url', response[1])
+        finally:
+            cleanup_file(filename)
+
+    def test_upload_to_submission_user(self, m):
+        register_uris({'assignment': ['upload_by_id', 'upload_final']}, m)
+
+        filename = 'testfile_assignment_{}'.format(uuid.uuid4().hex)
+
+        user_id = 1
+
+        try:
+            with open(filename, 'w+') as file:
+                response = self.assignment.upload_to_submission(file, user_id)
+
+            self.assertTrue(response[0])
+            self.assertIsInstance(response[1], dict)
+            self.assertIn('url', response[1])
+        finally:
+            cleanup_file(filename)
+
 
 @requests_mock.Mocker()
 class TestAssignmentGroup(unittest.TestCase):
diff --git a/tests/test_util.py b/tests/test_util.py
index 0feda10..6a9d835 100644
--- a/tests/test_util.py
+++ b/tests/test_util.py
@@ -10,7 +10,7 @@ from canvasapi.util import (
     combine_kwargs, get_institution_url, is_multivalued, obj_or_id
 )
 from itertools import chain
-from six import integer_types, iterkeys, itervalues, iteritems
+from six import integer_types, iterkeys, itervalues, iteritems, text_type
 from six.moves import zip
 from tests import settings
 from tests.util import register_uris
@@ -421,6 +421,26 @@ class TestUtil(unittest.TestCase):
         with self.assertRaises(TypeError):
             obj_or_id(nick, 'nickname_id', (CourseNickname,))
 
+    def test_obj_or_id_multiple_objs(self, m):
+        register_uris({'user': ['get_by_id']}, m)
+
+        user = self.canvas.get_user(1)
+
+        user_id = obj_or_id(user, 'user_id', (CourseNickname, User))
+
+        self.assertIsInstance(user_id, int)
+        self.assertEqual(user_id, 1)
+
+    def test_obj_or_id_user_self(self, m):
+        user_id = obj_or_id('self', 'user_id', (User,))
+
+        self.assertIsInstance(user_id, text_type)
+        self.assertEqual(user_id, 'self')
+
+    def test_obj_or_id_nonuser_self(self, m):
+        with self.assertRaises(TypeError):
+            obj_or_id('self', 'user_id', (CourseNickname,))
+
     # get_institution_url()
     def test_get_institution_url(self, m):
         correct_url = 'https://my.canvas.edu'

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest
apt-get update apt-get install -y gcc
3.7
requirements.txt dev_requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.13
Babel==2.14.0
-e git+https://github.com/ucfopen/canvasapi.git@4b14ed0ad55ff1e729ecfb678b40f3a0306c2a83#egg=canvasapi
certifi @ file:///croot/certifi_1671487769961/work/certifi
charset-normalizer==3.4.1
coverage==7.2.7
docutils==0.17.1
exceptiongroup==1.2.2
flake8==5.0.4
idna==3.10
imagesize==1.4.1
importlib-metadata==4.2.0
iniconfig==2.0.0
Jinja2==3.1.6
MarkupSafe==2.1.5
mccabe==0.7.0
packaging==24.0
pluggy==1.2.0
pycodestyle==2.9.1
pyflakes==2.5.0
Pygments==2.17.2
pytest==7.4.4
pytz==2025.2
requests==2.31.0
requests-mock==1.12.1
six==1.17.0
snowballstemmer==2.2.0
Sphinx==4.3.2
sphinx-rtd-theme==1.3.0
sphinxcontrib-applehelp==1.0.2
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==2.0.0
sphinxcontrib-jquery==4.1
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==1.0.3
sphinxcontrib-serializinghtml==1.1.5
tomli==2.0.1
typing_extensions==4.7.1
urllib3==2.0.7
zipp==3.15.0

name: canvasapi
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=22.3.1=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.13
      - babel==2.14.0
      - charset-normalizer==3.4.1
      - coverage==7.2.7
      - docutils==0.17.1
      - exceptiongroup==1.2.2
      - flake8==5.0.4
      - idna==3.10
      - imagesize==1.4.1
      - importlib-metadata==4.2.0
      - iniconfig==2.0.0
      - jinja2==3.1.6
      - markupsafe==2.1.5
      - mccabe==0.7.0
      - packaging==24.0
      - pluggy==1.2.0
      - pycodestyle==2.9.1
      - pyflakes==2.5.0
      - pygments==2.17.2
      - pytest==7.4.4
      - pytz==2025.2
      - requests==2.31.0
      - requests-mock==1.12.1
      - six==1.17.0
      - snowballstemmer==2.2.0
      - sphinx==4.3.2
      - sphinx-rtd-theme==1.3.0
      - sphinxcontrib-applehelp==1.0.2
      - sphinxcontrib-devhelp==1.0.2
      - sphinxcontrib-htmlhelp==2.0.0
      - sphinxcontrib-jquery==4.1
      - sphinxcontrib-jsmath==1.0.1
      - sphinxcontrib-qthelp==1.0.3
      - sphinxcontrib-serializinghtml==1.1.5
      - tomli==2.0.1
      - typing-extensions==4.7.1
      - urllib3==2.0.7
      - zipp==3.15.0
prefix: /opt/conda/envs/canvasapi

tests/test_assignment.py::TestAssignment::test_submit_file tests/test_assignment.py::TestAssignment::test_submit_file_upload_failure tests/test_assignment.py::TestAssignment::test_submit_file_wrong_type tests/test_assignment.py::TestAssignment::test_upload_to_submission_self tests/test_assignment.py::TestAssignment::test_upload_to_submission_user tests/test_util.py::TestUtil::test_obj_or_id_user_self
tests/test_assignment.py::TestAssignment::test__str__ tests/test_assignment.py::TestAssignment::test_delete_assignments tests/test_assignment.py::TestAssignment::test_edit_assignment tests/test_assignment.py::TestAssignment::test_get_gradeable_students tests/test_assignment.py::TestAssignment::test_get_submission tests/test_assignment.py::TestAssignment::test_get_submissions tests/test_assignment.py::TestAssignment::test_submissions_bulk_update tests/test_assignment.py::TestAssignment::test_submit tests/test_assignment.py::TestAssignment::test_submit_fail tests/test_assignment.py::TestAssignmentGroup::test__str__ tests/test_assignment.py::TestAssignmentGroup::test_delete_assignment_group tests/test_assignment.py::TestAssignmentGroup::test_edit_assignment_group tests/test_util.py::TestUtil::test_combine_kwargs_empty tests/test_util.py::TestUtil::test_combine_kwargs_multiple_dicts tests/test_util.py::TestUtil::test_combine_kwargs_multiple_mixed tests/test_util.py::TestUtil::test_combine_kwargs_multiple_nested_dicts tests/test_util.py::TestUtil::test_combine_kwargs_nested_dict tests/test_util.py::TestUtil::test_combine_kwargs_single tests/test_util.py::TestUtil::test_combine_kwargs_single_dict tests/test_util.py::TestUtil::test_combine_kwargs_single_generator_empty tests/test_util.py::TestUtil::test_combine_kwargs_single_generator_multiple_items tests/test_util.py::TestUtil::test_combine_kwargs_single_generator_single_item tests/test_util.py::TestUtil::test_combine_kwargs_single_list_empty tests/test_util.py::TestUtil::test_combine_kwargs_single_list_multiple_items tests/test_util.py::TestUtil::test_combine_kwargs_single_list_single_item tests/test_util.py::TestUtil::test_combine_kwargs_super_nested_dict tests/test_util.py::TestUtil::test_combine_kwargs_the_gauntlet tests/test_util.py::TestUtil::test_get_institution_url tests/test_util.py::TestUtil::test_is_multivalued_bool tests/test_util.py::TestUtil::test_is_multivalued_bytes tests/test_util.py::TestUtil::test_is_multivalued_chain tests/test_util.py::TestUtil::test_is_multivalued_dict tests/test_util.py::TestUtil::test_is_multivalued_dict_items tests/test_util.py::TestUtil::test_is_multivalued_dict_iter tests/test_util.py::TestUtil::test_is_multivalued_dict_keys tests/test_util.py::TestUtil::test_is_multivalued_dict_values tests/test_util.py::TestUtil::test_is_multivalued_generator_call tests/test_util.py::TestUtil::test_is_multivalued_generator_expr tests/test_util.py::TestUtil::test_is_multivalued_integer_types tests/test_util.py::TestUtil::test_is_multivalued_list tests/test_util.py::TestUtil::test_is_multivalued_list_iter tests/test_util.py::TestUtil::test_is_multivalued_set tests/test_util.py::TestUtil::test_is_multivalued_set_iter tests/test_util.py::TestUtil::test_is_multivalued_str tests/test_util.py::TestUtil::test_is_multivalued_tuple tests/test_util.py::TestUtil::test_is_multivalued_tuple_iter tests/test_util.py::TestUtil::test_is_multivalued_unicode tests/test_util.py::TestUtil::test_is_multivalued_zip tests/test_util.py::TestUtil::test_obj_or_id_int tests/test_util.py::TestUtil::test_obj_or_id_multiple_objs tests/test_util.py::TestUtil::test_obj_or_id_nonuser_self tests/test_util.py::TestUtil::test_obj_or_id_obj tests/test_util.py::TestUtil::test_obj_or_id_obj_no_id tests/test_util.py::TestUtil::test_obj_or_id_str_invalid tests/test_util.py::TestUtil::test_obj_or_id_str_valid
canvasapi/util.py canvasapi/upload.py canvasapi/assignment.py CHANGELOG.md
canvasapi/util.py canvasapi/upload.py canvasapi/assignment.py CHANGELOG.md