If you get back large results sets that you use for further processing PyODBC will be better suited.
But for a small number of values, the overhead at both ends to service ODBC structures may not pay off
since both ends have to get their internal structure in to  ODBC and out of it.
I don't have measured the difference so this is just a guess:
- for the typical embedded SQL returning < 1..10 rows a MethodCall might be more efficient.
This doesn't prevent you from using and tuning an SQL SELECT isolated in IRIS environment. 
In any case, the transfer between PY and IRIS is the slowest piece.
The less data you transport the faster the action is completed.
And transport in blocks wins over isolated pieces in loops.

@Chris Bransden 
Without knowing the definition of CUSTOM_MyQuery(par) it's no possible to answer.
The error message indicates that a literal is expected but indeed TableA.ID is a column reference and you feed a whole resultset instead of a single value

My interpretation: You want to see the rows  found by   CUSTOM_MyQuery()
which is indeed a classical inner join. 

So what is the result returned by  SELECT * FROM CUSTOM_MyQuery(??)  ?

You may try this transformation  that does the same in principle

SELECT *  FROM TableA
WHERE 0<(SELECT count(*) FROM CUSTOM_MyQuery(TableA.ID))

There is a bunch of auto-generated methods that might be useful:
https://community.intersystems.com/post/useful-auto-generated-methods

especially this one from @Eduard Lebedyuk 
#############################################################

But with PropertySetObjectId you can expedite things

set person = ##class(Person).%New()
set companyId = 123
do person.EmployedAtSetObjectId(companyId)

The main advantage is that company object doesn't have to be opened.

#############################################################