1+ function setupPythonEnv(varargin )
2+ % setupPythonEnv configures a Python environment compatible with MATLAB.
3+ %
4+ % <a href="matlab: docsearchFS('setupPythonEnv')">Link to the help function</a>
5+ %
6+ % This function detects a Python installation compatible with the current
7+ % MATLAB release, excludes unsupported system interpreters (such as the
8+ % Python distributed with Xcode on macOS), configures pyenv accordingly,
9+ % and optionally executes a pip command in the selected Python environment.
10+ %
11+ % Required input arguments:
12+ %
13+ % None.
14+ %
15+ % Optional input arguments:
16+ %
17+ % PipCommand : Pip command to be executed in the selected Python
18+ % environment. If PipCommand does not start with a standard
19+ % pip action such as 'install', 'uninstall', 'list' or
20+ % 'show', and contains a single token only, it is
21+ % interpreted as a package name and automatically expanded
22+ % to 'install <package>'.
23+ % Example - 'PipCommand','install yfinance'
24+ % Example - 'PipCommand','yfinance'
25+ % Data Types - char | string
26+ %
27+ % Verbose : Logical scalar which controls the display of diagnostic
28+ % messages.
29+ % Default is true.
30+ % Example - 'Verbose',false
31+ % Data Types - logical
32+ %
33+ % Output:
34+ %
35+ % This function does not return output arguments. It configures the
36+ % Python environment for the current MATLAB session and optionally
37+ % executes a pip command.
38+ %
39+ % See also: pyenv, pyrun, system
40+ %
41+ % Copyright 2008-2026.
42+ % Written by FSDA team
43+ %
44+ % <a href="matlab: docsearchFS('setupPythonEnv')">Link to the help page for this function</a>
45+ %
46+ % $LastChangedDate:: $: Date of the last commit
47+ %
48+
49+ % Examples:
50+ %
51+ %{
52+ % Just configure a compatible Python environment.
53+ setupPythonEnv
54+ %}
55+
56+ %{
57+ % Install Python package yfinance in the selected environment.
58+ setupPythonEnv('PipCommand','install yfinance');
59+ %}
60+
61+ %{
62+ % Equivalent compact syntax: if a single package name is supplied, it
63+ % is interpreted as an install request.
64+ setupPythonEnv('PipCommand','yfinance');
65+ %}
66+
67+ %{
68+ % Show the list of installed packages.
69+ setupPythonEnv('PipCommand','list');
70+ %}
71+
72+ %{
73+ % Show reduced output.
74+ setupPythonEnv('PipCommand','show yfinance','Verbose',false);
75+ %}
76+
77+ %% Beginning of code
78+
79+ PipCommand = ' ' ;
80+ Verbose = true ;
81+
82+ if nargin > 0
83+
84+ options = struct(' PipCommand' ,PipCommand ,' Verbose' ,Verbose );
85+ [varargin{: }] = convertStringsToChars(varargin{: });
86+ UserOptions = varargin(1 : 2 : length(varargin ));
87+
88+ if ~isempty(UserOptions )
89+ if length(varargin ) ~= 2 * length(UserOptions )
90+ error(' FSDA:setupPythonEnv:WrongInputOpt' , ...
91+ [' Number of supplied options is invalid. Probably values ' ...
92+ ' for some parameters are missing.' ]);
93+ end
94+ aux .chkoptions(options ,UserOptions );
95+ end
96+
97+ for i = 1 : 2 : length(varargin )
98+ options.(varargin{i }) = varargin{i + 1 };
99+ end
100+
101+ PipCommand = options .PipCommand;
102+ Verbose = options .Verbose;
103+ end
104+
105+ % 1. Check operating system, detect a compatible Python interpreter and
106+ % configure pyenv if needed.
107+ try
108+ [setup_success , python_exe ] = run_system_checks(Verbose );
109+ catch ME
110+ error(' FSDA:setupPythonEnv:SystemCheckError' , ...
111+ ' Critical error during system analysis: %s ' , ME .message);
112+ end
113+
114+ if ~setup_success
115+ if Verbose
116+ disp(' Operation aborted. Python requirements not met.' );
117+ end
118+ return ;
119+ end
120+
121+ % 2. Optionally execute a pip command in the selected environment.
122+ if ~isempty(PipCommand )
123+
124+ PipCommand = strtrim(char(PipCommand ));
125+ pipCmdPadded = [PipCommand ' ' ];
126+
127+ isExplicitCmd = startsWith(pipCmdPadded ,' install ' ) || ...
128+ startsWith(pipCmdPadded ,' uninstall ' ) || ...
129+ startsWith(pipCmdPadded ,' list ' ) || ...
130+ startsWith(pipCmdPadded ,' show ' ) || ...
131+ strcmp(PipCommand ,' list' ) || ...
132+ startsWith(PipCommand ,' --version' );
133+
134+ if ~isExplicitCmd && ~contains(PipCommand ,' ' )
135+ full_command = [' install ' PipCommand ];
136+ else
137+ full_command = PipCommand ;
138+ end
139+
140+ sys_cmd = [' "' char(python_exe ) ' " -m pip ' full_command ];
141+
142+ if Verbose
143+ disp([' Internal pip execution: ' sys_cmd ]);
144+ end
145+
146+ [status , cmdout ] = system(sys_cmd );
147+
148+ if status == 0
149+ if Verbose && ~isempty(strtrim(cmdout ))
150+ disp(cmdout );
151+ end
152+ else
153+ error(' FSDA:setupPythonEnv:PipExecutionError' , ...
154+ ' Error during pip execution:\n%s ' , cmdout );
155+ end
156+ end
157+
158+ end
159+
160+ % -------------------------------------------------------------------------
161+ % Subfunctions
162+ % -------------------------------------------------------------------------
163+
164+ function [success , valid_path ] = run_system_checks(Verbose )
165+
166+ success = false ;
167+ valid_path = " " ;
168+
169+ % Extract current MATLAB release, for example '2024b'.
170+ m_rel = version(' -release' );
171+ pe = pyenv ;
172+
173+ % Anonymous function to identify Python interpreters associated with Xcode.
174+ is_xcode = @(p ) contains(p ,' Xcode.app' ) || strcmp(p ,' /usr/bin/python3' );
175+
176+ % If pyenv is already loaded, reuse it unless it points to the Xcode
177+ % Python interpreter on macOS.
178+ if pe .Executable ~= " " && pe .Status == " Loaded"
179+ if ismac && is_xcode(char(pe .Executable))
180+ error([' MATLAB has already loaded the Xcode Python interpreter. ' ...
181+ ' Restart MATLAB to apply the bypass.' ]);
182+ else
183+ success = true ;
184+ valid_path = char(pe .Executable);
185+ return ;
186+ end
187+ end
188+
189+ if Verbose
190+ disp(' --- Starting Python Environment Analysis ---' );
191+ end
192+
193+ if ispc
194+
195+ if Verbose
196+ disp(' Operating System: Windows' );
197+ end
198+
199+ paths = get_system_paths(' windows' );
200+ [valid_path , python_exists ] = find_compatible_python(paths ,m_rel ,false );
201+
202+ if ~python_exists
203+ if Verbose
204+ disp(' -> No Python installation detected on the system.' );
205+ suggest_installation(m_rel );
206+ end
207+ elseif valid_path == " "
208+ if Verbose
209+ disp([' -> Python is installed, but the version is not ' ...
210+ ' compatible with the MATLAB release.' ]);
211+ suggest_installation(m_rel );
212+ end
213+ else
214+ success = true ;
215+ end
216+
217+ elseif ismac
218+
219+ if Verbose
220+ disp(' Operating System: macOS' );
221+ end
222+
223+ [xcode_status , ~ ] = system(' xcode-select -p 2>/dev/null' );
224+ xcode_installed = (xcode_status == 0 );
225+ paths = get_system_paths(' macos' );
226+
227+ if ~xcode_installed
228+
229+ if Verbose
230+ disp(' Xcode Status: Not installed.' );
231+ end
232+
233+ [valid_path , python_exists ] = find_compatible_python(paths ,m_rel ,false );
234+
235+ if ~python_exists
236+ if Verbose
237+ disp(' -> Python Detection: No installation found.' );
238+ suggest_installation(m_rel );
239+ end
240+ elseif valid_path == " "
241+ if Verbose
242+ disp(' -> Python Detection: Versions found but not compatible with MATLAB.' );
243+ suggest_installation(m_rel );
244+ end
245+ else
246+ success = true ;
247+ end
248+
249+ else
250+
251+ if Verbose
252+ disp(' Xcode Status: Installed (activating system dependencies bypass).' );
253+ end
254+
255+ [valid_path , python_exists ] = find_compatible_python(paths ,m_rel ,true );
256+
257+ if ~python_exists
258+ if Verbose
259+ disp(' -> Python Detection: No valid non-Xcode installation found.' );
260+ suggest_installation(m_rel );
261+ end
262+ elseif valid_path == " "
263+ if Verbose
264+ disp(' -> Python Detection: Independent installations are not compatible.' );
265+ suggest_installation(m_rel );
266+ end
267+ else
268+ success = true ;
269+ end
270+ end
271+
272+ else
273+ error(' FSDA:setupPythonEnv:UnsupportedOS' , ...
274+ ' Operating system not supported.' );
275+ end
276+
277+ if success && pe .Executable == " "
278+ pyenv(' Version' ,valid_path );
279+ if Verbose
280+ disp([' -> Configuration completed. Executable in use: ' valid_path ]);
281+ end
282+ end
283+
284+ end
285+
286+ function paths = get_system_paths(os_type )
287+
288+ if strcmp(os_type ,' windows' )
289+ [~ , cmdout ] = system(' where python python3 python3.13 2>nul' );
290+ fallback = " " ;
291+ else
292+ [~ , cmdout ] = system(' /bin/zsh -lc "which -a python3 python3.13 2>/dev/null"' );
293+ fallback = sprintf([' \n /opt/homebrew/bin/python3' ...
294+ ' \n /opt/homebrew/bin/python3.13' ...
295+ ' \n /usr/local/bin/python3' ...
296+ ' \n /Library/Frameworks/Python.framework/Versions/3.13/bin/python3.13' ]);
297+ end
298+
299+ raw_string = string(cmdout ) + string(fallback );
300+ paths = unique(split(raw_string ,newline ),' stable' );
301+
302+ end
303+
304+ function [valid_path , any_python_exists ] = find_compatible_python(paths ,m_rel ,bypass_xcode )
305+
306+ valid_path = " " ;
307+ any_python_exists = false ;
308+
309+ for i = 1 : length(paths )
310+
311+ curr_str = strtrim(paths(i ));
312+
313+ if curr_str == " " || ~isfile(curr_str )
314+ continue
315+ end
316+
317+ curr_char = char(curr_str );
318+
319+ if bypass_xcode && ...
320+ (contains(curr_char ,' Xcode.app' ) || strcmp(curr_char ,' /usr/bin/python3' ))
321+ continue
322+ end
323+
324+ [status , ver_str ] = system([' "' curr_char ' " --version' ]);
325+
326+ if status == 0
327+ any_python_exists = true ;
328+ if check_version_compatibility(ver_str ,m_rel )
329+ valid_path = curr_char ;
330+ break
331+ end
332+ end
333+ end
334+
335+ end
336+
337+ function is_compat = check_version_compatibility(ver_str ,m_rel )
338+
339+ tokens = regexp(string(ver_str ),' 3\.\d+' ,' match' );
340+
341+ if isempty(tokens )
342+ is_compat = false ;
343+ return
344+ end
345+
346+ py_ver_short = tokens(1 );
347+
348+ switch m_rel
349+ case {' 2026a' ,' 2025b' }
350+ compat_list = [" 3.10" ," 3.11" ," 3.12" ," 3.13" ];
351+ case {' 2025a' ,' 2024b' }
352+ compat_list = [" 3.9" ," 3.10" ," 3.11" ," 3.12" ];
353+ case {' 2024a' ,' 2023b' }
354+ compat_list = [" 3.9" ," 3.10" ," 3.11" ];
355+ case {' 2023a' ,' 2022b' }
356+ compat_list = [" 3.8" ," 3.9" ," 3.10" ];
357+ case {' 2022a' ,' 2021b' }
358+ compat_list = [" 3.7" ," 3.8" ," 3.9" ];
359+ otherwise
360+ compat_list = [];
361+ end
362+
363+ is_compat = ismember(py_ver_short ,compat_list );
364+
365+ end
366+
367+ function suggest_installation(m_rel )
368+
369+ disp(' ---------------------------------------------------------' );
370+ disp([' ACTION REQUIRED: Install a Python version compatible with MATLAB R' m_rel ]);
371+ disp(' Download: https://www.python.org/downloads/' );
372+
373+ switch m_rel
374+ case {' 2026a' ,' 2025b' }
375+ disp(' Supported: 3.10, 3.11, 3.12, 3.13' );
376+ case {' 2025a' ,' 2024b' }
377+ disp(' Supported: 3.9, 3.10, 3.11, 3.12' );
378+ case {' 2024a' ,' 2023b' }
379+ disp(' Supported: 3.9, 3.10, 3.11' );
380+ case {' 2023a' ,' 2022b' }
381+ disp(' Supported: 3.8, 3.9, 3.10' );
382+ case {' 2022a' ,' 2021b' }
383+ disp(' Supported: 3.7, 3.8, 3.9' );
384+ otherwise
385+ disp(' MATLAB version not classified within the R2021b-R2026a range.' );
386+ end
387+
388+ disp(' ---------------------------------------------------------' );
389+
390+ end
0 commit comments