Inheriting legacy code is one of the most common things in the software industry. So common that, we can find several publications regarding this topic. Worth a mention Working effectively with legacy code - Michael C. Feathers for being one of the most renowned.
However, these publications are strongly focused on refactoring code. If I understood right, that's not your goal, your goal is to document.
With this goal in mind, here some tips that might help you.
1. Get a copy of the code
Get a copy of the code in production. Fork the code in the SCM or make a branch and check it out.
2. Contextualize the code
Decontextualized code doesn't say too much about its intentions. It tells how things are done, but it doesn't say too much about the why (requirements) and when (use cases).
Any piece of code serves a purpose. The sum of many pieces serves a higher purpose. And so on. The first task is to find out the highest purpose. The overall idea (what problem is solving). Then we have to contextualize it inside the system in order to answer the why and the when.
These answers are important for the future tests. Without them, we could end up testing irrelevant uses cases or misinterpret the results.
This irremediably lead us to ask people that
have very little time to talk to us about it. But, the functional knowledge is as important (or more) as the technical, keep asking3.
3. Identifying levels of abstractions (responsibilities)
If we found out the global idea, we already identified the highest abstraction. As we delve into the code, we find more and lower levels of abstractions. And by abstractions, I mean responsibilities.1
The goal here is to identify the components involved in each responsibility and how they contribute to the global solution.
4. Document the overall picture
Provide future developers with a little introduction of the code.
Add a README-like file to the project and put in it the information gathered during the contextualization. Introduce the global picture, the most relevant abstractions and why are they relevant.
Hints for running the applications and reproduce the use cases are highly appreciated.
5. Document the code
Having identified the responsibilities of the components now is time to document them in the code. Take a look at the documentation conventions of the language (Python Docstrings). Then write the code documentation according to these conventions. Be clear and concise.
6. Document traps and misleading names
No need to say that most of us are terrible when it comes to naming things. Not everybody craft self-documented or self-descriptive code. So, don't you blindly trust everything you read. Get into the functions or classes. If the components names are not aligned with their respective functionalities, document them but don't change the actual names.
7. Document dependencies
Be back to the Readme file and introduce the dependencies (3rd party libraries). Where and when are they used, versions, etc. Add links (if possible) to the official pages of the libraries.
8. Document integrations
I use to introduce the integrations with diagrams. These are much more expressive than textual documents.
Additionally, for each integration could be useful to document request/response messages, ER data models, data sources, URLs, endpoints, protocols, etc. The more info you gather the better.
Especial attention to the error handling strategies. They provide valuable information about how the business reacts to errors.
9. Test the code
This's easier to say than done. Most of the legacy code we inherit was not written to be testable. Considering the future migration and the actual task (documenting), I'm unsure about the value provided by the unit tests.
For this particular case, integration and end-to-end are more valuable than unit tests because the actual components might not be reused during the migration (normally they aren't). The migration could completely change the implementation 2.
Ask your project's manager how much time you have for completing this task. Implementing tests for legacy code uses to take longer than usual. So, address the tests to provide valuable insights of the business rather than the implementations details.
In case of doubts about the unit tests, I suggest taking a look at the following questions
10. Document inconsistencies
During the test phase, we might find inconsistencies between the results we got and the results we expected. It would be dangerous to assume that these inconsistencies are bugs. We could have found bugs or missing features not documented anywhere. Or we could have misinterpreted some requirements. In any case, we should document these cases. Especially, how to reproduce them.
11. Commit the changes frequently.
As the documentation progress, save the changes. Do frequent commits.
Note that I have not mentioned anything about executing and debugging the code. I think I could not say anything that hasn't already been said in:
1: Remember that any responsibility can be comprised in turn by one or more secondary responsibilities.
2: If the actual code is going to be reused (adapted) as it's, then unit tests are a must.
3: One strategy that works for me is inviting these persons to a coffee.