diff --git a/nbgrader/converters/autograde.py b/nbgrader/converters/autograde.py
index 62327f17..57c662ab 100644
--- a/nbgrader/converters/autograde.py
+++ b/nbgrader/converters/autograde.py
@@ -2,7 +2,7 @@ import os
 import shutil
 
 from textwrap import dedent
-from traitlets import Bool, List
+from traitlets import Bool, List, Dict
 
 from .base import BaseConverter, NbGraderException
 from ..preprocessors import (
@@ -24,6 +24,19 @@ class Autograde(BaseConverter):
         )
     ).tag(config=True)
 
+    exclude_overwriting = Dict(
+        {},
+        help=dedent(
+            """
+            A dictionary with keys corresponding to assignment names and values
+            being a list of filenames (relative to the assignment's source
+            directory) that should NOT be overwritten with the source version.
+            This is to allow students to e.g. edit a python file and submit it
+            alongside the notebooks in their assignment.
+            """
+        )
+    ).tag(config=True)
+
     _sanitizing = True
 
     @property
@@ -109,7 +122,9 @@ class Autograde(BaseConverter):
         self.log.info("Overwriting files with master versions from the source directory")
         dest_path = self._format_dest(assignment_id, student_id)
         source_path = self.coursedir.format_path(self.coursedir.source_directory, '.', assignment_id)
-        source_files = utils.find_all_files(source_path, self.coursedir.ignore + ["*.ipynb"])
+        source_files = set(utils.find_all_files(source_path, self.coursedir.ignore + ["*.ipynb"]))
+        exclude_files = set([os.path.join(source_path, x) for x in self.exclude_overwriting.get(assignment_id, [])])
+        source_files = list(source_files - exclude_files)
 
         # copy them to the build directory
         for filename in source_files:

Have submitted notebooks import from local directory
I had students edit a python file and then submit it along with the notebooks. However, when I run the autograder, nbgrader imports the python file from my source directory instead of the submitted one. This, of course, leads to the test cells that test their implementation always passing no matter what they do (and also makes it so that, if they added and rely on any further functionality that's not in my solution, then those blocks fail!). Is there any way to have the submitted notebooks import from the submitted .py file? 

diff --git a/nbgrader/tests/apps/test_nbgrader_autograde.py b/nbgrader/tests/apps/test_nbgrader_autograde.py
index 091564e7..ba44d44b 100644
--- a/nbgrader/tests/apps/test_nbgrader_autograde.py
+++ b/nbgrader/tests/apps/test_nbgrader_autograde.py
@@ -389,20 +389,24 @@ class TestNbGraderAutograde(BaseTestApp):
         """Are dependent files properly linked and overwritten?"""
         with open("nbgrader_config.py", "a") as fh:
             fh.write("""c.CourseDirectory.db_assignments = [dict(name='ps1', duedate='2015-02-02 14:58:23.948203 PST')]\n""")
-            fh.write("""c.CourseDirectory.db_students = [dict(id="foo"), dict(id="bar")]""")
+            fh.write("""c.CourseDirectory.db_students = [dict(id="foo"), dict(id="bar")]\n""")
+            fh.write("""c.Autograde.exclude_overwriting = {"ps1": ["helper.py"]}\n""")
 
         self._copy_file(join("files", "submitted-unchanged.ipynb"), join(course_dir, "source", "ps1", "p1.ipynb"))
         self._make_file(join(course_dir, "source", "ps1", "data.csv"), "some,data\n")
+        self._make_file(join(course_dir, "source", "ps1", "helper.py"), "print('hello!')\n")
         run_nbgrader(["assign", "ps1", "--db", db])
 
         self._copy_file(join("files", "submitted-unchanged.ipynb"), join(course_dir, "submitted", "foo", "ps1", "p1.ipynb"))
         self._make_file(join(course_dir, "submitted", "foo", "ps1", "timestamp.txt"), "2015-02-02 15:58:23.948203 PST")
         self._make_file(join(course_dir, "submitted", "foo", "ps1", "data.csv"), "some,other,data\n")
+        self._make_file(join(course_dir, "submitted", "foo", "ps1", "helper.py"), "print('this is different!')\n")
         run_nbgrader(["autograde", "ps1", "--db", db])
 
         assert os.path.isfile(join(course_dir, "autograded", "foo", "ps1", "p1.ipynb"))
         assert os.path.isfile(join(course_dir, "autograded", "foo", "ps1", "timestamp.txt"))
         assert os.path.isfile(join(course_dir, "autograded", "foo", "ps1", "data.csv"))
+        assert os.path.isfile(join(course_dir, "autograded", "foo", "ps1", "helper.py"))
 
         with open(join(course_dir, "autograded", "foo", "ps1", "timestamp.txt"), "r") as fh:
             contents = fh.read()
@@ -412,6 +416,45 @@ class TestNbGraderAutograde(BaseTestApp):
             contents = fh.read()
         assert contents == "some,data\n"
 
+        with open(join(course_dir, "autograded", "foo", "ps1", "helper.py"), "r") as fh:
+            contents = fh.read()
+        assert contents == "print('this is different!')\n"
+
+    def test_grade_overwrite_files_subdirs(self, db, course_dir):
+        """Are dependent files properly linked and overwritten?"""
+        with open("nbgrader_config.py", "a") as fh:
+            fh.write("""c.CourseDirectory.db_assignments = [dict(name='ps1', duedate='2015-02-02 14:58:23.948203 PST')]\n""")
+            fh.write("""c.CourseDirectory.db_students = [dict(id="foo"), dict(id="bar")]\n""")
+            fh.write("""c.Autograde.exclude_overwriting = {{"ps1": ["{}"]}}\n""".format(os.path.join("subdir", "helper.py")))
+
+        self._copy_file(join("files", "submitted-unchanged.ipynb"), join(course_dir, "source", "ps1", "p1.ipynb"))
+        self._make_file(join(course_dir, "source", "ps1", "subdir", "data.csv"), "some,data\n")
+        self._make_file(join(course_dir, "source", "ps1", "subdir", "helper.py"), "print('hello!')\n")
+        run_nbgrader(["assign", "ps1", "--db", db])
+
+        self._copy_file(join("files", "submitted-unchanged.ipynb"), join(course_dir, "submitted", "foo", "ps1", "p1.ipynb"))
+        self._make_file(join(course_dir, "submitted", "foo", "ps1", "timestamp.txt"), "2015-02-02 15:58:23.948203 PST")
+        self._make_file(join(course_dir, "submitted", "foo", "ps1", "subdir", "data.csv"), "some,other,data\n")
+        self._make_file(join(course_dir, "submitted", "foo", "ps1", "subdir", "helper.py"), "print('this is different!')\n")
+        run_nbgrader(["autograde", "ps1", "--db", db])
+
+        assert os.path.isfile(join(course_dir, "autograded", "foo", "ps1", "p1.ipynb"))
+        assert os.path.isfile(join(course_dir, "autograded", "foo", "ps1", "timestamp.txt"))
+        assert os.path.isfile(join(course_dir, "autograded", "foo", "ps1", "subdir", "data.csv"))
+        assert os.path.isfile(join(course_dir, "autograded", "foo", "ps1", "subdir", "helper.py"))
+
+        with open(join(course_dir, "autograded", "foo", "ps1", "timestamp.txt"), "r") as fh:
+            contents = fh.read()
+        assert contents == "2015-02-02 15:58:23.948203 PST"
+
+        with open(join(course_dir, "autograded", "foo", "ps1", "subdir", "data.csv"), "r") as fh:
+            contents = fh.read()
+        assert contents == "some,data\n"
+
+        with open(join(course_dir, "autograded", "foo", "ps1", "subdir", "helper.py"), "r") as fh:
+            contents = fh.read()
+        assert contents == "print('this is different!')\n"
+
     def test_side_effects(self, db, course_dir):
         with open("nbgrader_config.py", "a") as fh:
             fh.write("""c.CourseDirectory.db_assignments = [dict(name='ps1', duedate='2015-02-02 14:58:23.948203 PST')]\n""")

pip install -e .[dev]
parse_log_pytest
requirements.txt
pyenchant sphinxcontrib-spelling sphinx_rtd_theme nbval pytest
apt-get update apt-get install -y gcc
3.5
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.13
alembic==1.7.7
anyio==3.6.2
argon2-cffi==21.3.0
argon2-cffi-bindings==21.2.0
async-generator==1.10
attrs==22.2.0
Babel==2.11.0
backcall==0.2.0
bleach==4.1.0
certifi==2021.5.30
cffi==1.15.1
charset-normalizer==2.0.12
comm==0.1.4
contextvars==2.4
coverage==6.2
dataclasses==0.8
decorator==5.1.1
defusedxml==0.7.1
docutils==0.18.1
entrypoints==0.4
greenlet==2.0.2
idna==3.10
imagesize==1.4.1
immutables==0.19
importlib-metadata==4.8.3
importlib-resources==5.4.0
iniconfig==1.1.1
ipykernel==5.5.6
ipython==7.16.3
ipython-genutils==0.2.0
ipywidgets==7.8.5
jedi==0.17.2
Jinja2==3.0.3
json5==0.9.16
jsonschema==3.2.0
jupyter==1.1.1
jupyter-client==7.1.2
jupyter-console==6.4.3
jupyter-core==4.9.2
jupyter-server==1.13.1
jupyterlab==3.2.9
jupyterlab-pygments==0.1.2
jupyterlab-server==2.10.3
jupyterlab_widgets==1.1.11
Mako==1.1.6
MarkupSafe==2.0.1
mistune==0.8.4
nbclassic==0.3.5
nbclient==0.5.9
nbconvert==6.0.7
nbformat==5.1.3
-e git+https://github.com/jupyter/nbgrader.git@919c56a9782647a97bd03a0c9d6d0ac5633db0a3#egg=nbgrader
nbval==0.10.0
nest-asyncio==1.6.0
notebook==6.4.10
packaging==21.3
pandocfilters==1.5.1
parso==0.7.1
pexpect==4.9.0
pickleshare==0.7.5
pluggy==1.0.0
prometheus-client==0.17.1
prompt-toolkit==3.0.36
ptyprocess==0.7.0
py==1.11.0
pycparser==2.21
pyenchant==3.2.2
Pygments==2.14.0
pyparsing==3.1.4
pyrsistent==0.18.0
pytest==7.0.1
python-dateutil==2.9.0.post0
pytz==2025.2
pyzmq==25.1.2
requests==2.27.1
Send2Trash==1.8.3
six==1.17.0
sniffio==1.2.0
snowballstemmer==2.2.0
Sphinx==5.3.0
sphinx-rtd-theme==2.0.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
sphinxcontrib-spelling==7.7.0
SQLAlchemy==1.4.54
terminado==0.12.1
testpath==0.6.0
tomli==1.2.3
tornado==6.1
traitlets==4.3.3
typing_extensions==4.1.1
urllib3==1.26.20
wcwidth==0.2.13
webencodings==0.5.1
websocket-client==1.3.1
widgetsnbextension==3.6.10
zipp==3.6.0

name: nbgrader
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=2021.5.30=py36h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - 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=21.2.2=py36h06a4308_0
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.13
      - alembic==1.7.7
      - anyio==3.6.2
      - argon2-cffi==21.3.0
      - argon2-cffi-bindings==21.2.0
      - async-generator==1.10
      - attrs==22.2.0
      - babel==2.11.0
      - backcall==0.2.0
      - bleach==4.1.0
      - cffi==1.15.1
      - charset-normalizer==2.0.12
      - comm==0.1.4
      - contextvars==2.4
      - coverage==6.2
      - dataclasses==0.8
      - decorator==5.1.1
      - defusedxml==0.7.1
      - docutils==0.18.1
      - entrypoints==0.4
      - greenlet==2.0.2
      - idna==3.10
      - imagesize==1.4.1
      - immutables==0.19
      - importlib-metadata==4.8.3
      - importlib-resources==5.4.0
      - iniconfig==1.1.1
      - ipykernel==5.5.6
      - ipython==7.16.3
      - ipython-genutils==0.2.0
      - ipywidgets==7.8.5
      - jedi==0.17.2
      - jinja2==3.0.3
      - json5==0.9.16
      - jsonschema==3.2.0
      - jupyter==1.1.1
      - jupyter-client==7.1.2
      - jupyter-console==6.4.3
      - jupyter-core==4.9.2
      - jupyter-server==1.13.1
      - jupyterlab==3.2.9
      - jupyterlab-pygments==0.1.2
      - jupyterlab-server==2.10.3
      - jupyterlab-widgets==1.1.11
      - mako==1.1.6
      - markupsafe==2.0.1
      - mistune==0.8.4
      - nbclassic==0.3.5
      - nbclient==0.5.9
      - nbconvert==6.0.7
      - nbformat==5.1.3
      - nbval==0.10.0
      - nest-asyncio==1.6.0
      - notebook==6.4.10
      - packaging==21.3
      - pandocfilters==1.5.1
      - parso==0.7.1
      - pexpect==4.9.0
      - pickleshare==0.7.5
      - pluggy==1.0.0
      - prometheus-client==0.17.1
      - prompt-toolkit==3.0.36
      - ptyprocess==0.7.0
      - py==1.11.0
      - pycparser==2.21
      - pyenchant==3.2.2
      - pygments==2.14.0
      - pyparsing==3.1.4
      - pyrsistent==0.18.0
      - pytest==7.0.1
      - python-dateutil==2.9.0.post0
      - pytz==2025.2
      - pyzmq==25.1.2
      - requests==2.27.1
      - send2trash==1.8.3
      - six==1.17.0
      - sniffio==1.2.0
      - snowballstemmer==2.2.0
      - sphinx==5.3.0
      - sphinx-rtd-theme==2.0.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
      - sphinxcontrib-spelling==7.7.0
      - sqlalchemy==1.4.54
      - terminado==0.12.1
      - testpath==0.6.0
      - tomli==1.2.3
      - tornado==6.1
      - traitlets==4.3.3
      - typing-extensions==4.1.1
      - urllib3==1.26.20
      - wcwidth==0.2.13
      - webencodings==0.5.1
      - websocket-client==1.3.1
      - widgetsnbextension==3.6.10
      - zipp==3.6.0
prefix: /opt/conda/envs/nbgrader

nbgrader/tests/apps/test_nbgrader_autograde.py::TestNbGraderAutograde::test_grade_overwrite_files nbgrader/tests/apps/test_nbgrader_autograde.py::TestNbGraderAutograde::test_grade_overwrite_files_subdirs
nbgrader/tests/apps/test_nbgrader_autograde.py::TestNbGraderAutograde::test_force_single_notebook nbgrader/tests/apps/test_nbgrader_autograde.py::TestNbGraderAutograde::test_update_newer_single_notebook
nbgrader/tests/apps/test_nbgrader_autograde.py::TestNbGraderAutograde::test_help nbgrader/tests/apps/test_nbgrader_autograde.py::TestNbGraderAutograde::test_missing_student nbgrader/tests/apps/test_nbgrader_autograde.py::TestNbGraderAutograde::test_missing_assignment nbgrader/tests/apps/test_nbgrader_autograde.py::TestNbGraderAutograde::test_grade nbgrader/tests/apps/test_nbgrader_autograde.py::TestNbGraderAutograde::test_grade_timestamp nbgrader/tests/apps/test_nbgrader_autograde.py::TestNbGraderAutograde::test_grade_empty_timestamp nbgrader/tests/apps/test_nbgrader_autograde.py::TestNbGraderAutograde::test_late_submission_penalty_none nbgrader/tests/apps/test_nbgrader_autograde.py::TestNbGraderAutograde::test_late_submission_penalty_zero nbgrader/tests/apps/test_nbgrader_autograde.py::TestNbGraderAutograde::test_late_submission_penalty_plugin nbgrader/tests/apps/test_nbgrader_autograde.py::TestNbGraderAutograde::test_force nbgrader/tests/apps/test_nbgrader_autograde.py::TestNbGraderAutograde::test_filter_notebook nbgrader/tests/apps/test_nbgrader_autograde.py::TestNbGraderAutograde::test_side_effects nbgrader/tests/apps/test_nbgrader_autograde.py::TestNbGraderAutograde::test_skip_extra_notebooks nbgrader/tests/apps/test_nbgrader_autograde.py::TestNbGraderAutograde::test_permissions nbgrader/tests/apps/test_nbgrader_autograde.py::TestNbGraderAutograde::test_custom_permissions nbgrader/tests/apps/test_nbgrader_autograde.py::TestNbGraderAutograde::test_update_newer nbgrader/tests/apps/test_nbgrader_autograde.py::TestNbGraderAutograde::test_hidden_tests_single_notebook nbgrader/tests/apps/test_nbgrader_autograde.py::TestNbGraderAutograde::test_handle_failure nbgrader/tests/apps/test_nbgrader_autograde.py::TestNbGraderAutograde::test_handle_failure_single_notebook nbgrader/tests/apps/test_nbgrader_autograde.py::TestNbGraderAutograde::test_missing_source_kernelspec nbgrader/tests/apps/test_nbgrader_autograde.py::TestNbGraderAutograde::test_incorrect_source_kernelspec nbgrader/tests/apps/test_nbgrader_autograde.py::TestNbGraderAutograde::test_incorrect_submitted_kernelspec nbgrader/tests/apps/test_nbgrader_autograde.py::TestNbGraderAutograde::test_no_execute nbgrader/tests/apps/test_nbgrader_autograde.py::TestNbGraderAutograde::test_infinite_loop nbgrader/tests/apps/test_nbgrader_autograde.py::TestNbGraderAutograde::test_missing_files nbgrader/tests/apps/test_nbgrader_autograde.py::TestNbGraderAutograde::test_grade_missing_notebook
nbgrader/converters/autograde.py
nbgrader/converters/autograde.py