When doing Data Science projects, I often have to load data and metadata, and output results, plots, logs, etc.
Therefore I have to handle all the file paths from where to load the input and write the output.
I don't think that hardcoding the paths in the script is a good practice, therefore I write them in a JSON file like this (UPDATED in order to make the below code run)
{"raw_data_analysis": {"log_path": "outputs/data_analysis/raw/data_analysis_log.txt",
"dataset_path": "data/raw/raw_dataset.csv",
"variables_metadata_path": "data/raw/variables_metadata.json",
"csv_output_folder": "outputs/data_analysis/raw/csv_files",
"markdown_output_folder": "outputs/data_analysis/raw/markdown_tables"
},
"clean_data_analysis": {
"dataset": "data/clean/clean_dataset.csv",
"variables_metadata_path": "data/clean/intermediate_variables_metadata.json",
"csv_output_folder": "outputs/data_analysis/clean/csv_files",
"table_output_folder": "outputs/data_analysis/cleaned/markdown_tables",
"plots_output_folder": "outputs/data_analysis/cleaned/plots"
}
}
Then i made a function that, given the path of the JSON file as command line argument with --paths_configuration_path
, and the key of the paths i want to read, returns a dictionary with as keys the "description" of the path and as value the path. This function also accepts a value for a default path of the JSON file, in case i don't give it from command line.
import argparse
import json
from pathlib import Path
def get_paths_config_from_cl_arguments(default_config_path: Path | None, default_key:str=None)->dict:
parser = argparse.ArgumentParser()
parser.add_argument("--paths_configuration_path", type=str,
default=default_config_path,
help="Path to the paths configuration file")
parser.add_argument("--key", type=str,
default=default_key,
help="key for the paths in the configuration file")
args = parser.parse_args()
paths_configuration_path=args.paths_configuration_path
key=args.key
paths = get_paths_from_configuration(paths_configuration_path=paths_configuration_path, key=key)
return paths
def get_paths_from_configuration(paths_configuration_path:Path,key:str|None)->dict:
with open(paths_configuration_path,'r') as file:
paths_configuration=json.load(file)
if key:
paths_configuration=paths_configuration[key]
return {key:Path(value) for key,value in paths_configuration.items()}
At this point, in the python scripts that i would actually run, I call this function giving as default path the one that i usually need, and then I extract the paths from the dictionary (In order for the velow code to run, you need the above JSON at the path configurations/data_analysis/data_analysis_paths_config.json
)
def main():
paths = get_paths_config_from_cl_arguments(
default_config_path=Path('configurations/data_analysis/data_analysis_paths_config.json'),
default_key='clean_data_analysis')
data_path = paths['dataset_path']
variables_metadata_path = paths['variables_description_path']
csv_output_folder = paths['csv_output_folder']
markdown_output_folder = paths['table_output_folder']
plots_output_folder = paths['plots_output_folder']
if __name__=='__main__':
main()
In this way I am free to run my script with the default paths, or i can just create a new JSON file with other paths and just pass the path to that JSON file as command line argument when i run the script.
What do you think about this approach? Is it clean and readable? Am I introducing too many functions and complicating a rather trivial problem? Is there any established design pattern that i should use? What is the standard approach for handling file paths?