Microsoft Graph

Using Microsoft Graph from Python

I’ve spent a lot of time using the Microsoft Graph for interacting with Azure Active Directory, Intune, and Windows Autopilot.  Most of this has been done using PowerShell (scripting) or C# (programming).  But as I got questions about using Python from my son (college classes) and others, I decided to spend a little of my summer vacation learning something new.

Since the easiest way to learn is through practical experience (for me at least), I thought that I’d try to use Python to interact with the Microsoft Graph.  How hard can that be?  There are multiple pieces to that:

  • The language itself.  The Python language is relatively straight-forward:  It is case-sensitive and leverages indention for nested flows.  It would be best described as an “interpreted programming language” (as compared to PowerShell, which is a case-insensitive scripting language).
  • The extensibility.  Your productivity is governed by how much you need to do yourself, versus what you can borrow/steal/use from others.  In the Python world, you leverage modules (a single Python script file that defines functions or classes that you can use) or packages (a collection of modules bundled together).  These are obtained using utilities like pip, which can pull them from a public repository – very convenient.  (This is similar to the PowerShell Gallery or other NuGet repositories.)
  • The specifics of what you want to do.  For example, to talk to the Microsoft Graph, I need to be able to authenticate to Azure AD, call REST methods, and work with JSON, so I needed to know how to do each of those using Python.

So let’s walk through that step by step, starting from the beginning with installing Python itself.  I installed that manually by downloading it from the main Python site. That installed both the x86 and x64 versions of Python, and added some entries to the PATH to point to them:


Notice that it installed per user (in my user profile) too.

The next step is to authenticate with Azure AD.  The Microsoft Authentication Library, a.k.a. MSAL, has a Python version available.  So that can be installed from a command line using “pip install msal”.  I installed that and then tried to use it and got an error that it wasn’t found when trying to import it.  Interestingly, it was installed by default as a 32-bit module, not a 64-bit one.  OK, so it seems the preference is to use 32-bit Python.  I’m OK with that – seems like most people prefer the 32-bit version if they aren’t doing things that require lots of RAM.

To make running Python easier, I added an entry to the Windows Terminal configuration:


(The starting directory was something I added to make testing easier, more on that later.  I also left out an icon, because it wasn’t necessary.)  Once that entry is added and the file is saved, you can see it show up in the Terminal menu:


And then you can select it to start a Python session:


So, back to MSAL.  I initially thought I would use a similar method to what I use with PowerShell:  Call a “connect” method that prompts for credentials and authenticates to Microsoft Graph automatically.  In the Python case, there is no UI provided by MSAL to do this.  I could have used a username and password, but the MSAL docs frown on that.  OK, so I looked at app-based authentication.  If you read my previous blog on that, you’ll know that you need to do some work in Azure AD to create an app, then create an app secret (basically, a password) for that app that you can leverage. 

Note that there is one additional API permission that needs to be added, beyond what was described in that blog.  You also need to include “DeviceManagementManagedDevices.ReadWrite.All” in order to query the list of Autopilot devices.  I suppose that makes sense, as each Autopilot device references an Intune device, but when I did that initial blog that wasn’t required.


Once you have that, logic like this will authenticate with Microsoft Graph:


You can pick up on some of the language details here too:  Variables don’t need to be declared (unless you want to use global variables inside locally-scoped functions).  Everything is case-sensitive.  All blocks (including if/then/else) use indention to show the flow.  Arrays are defined using brackets.  Dictionaries (equivalent to hashtables in PowerShell) are defined using curly braces.  Parameters can be passed to functions or constructors positionally (e.g. the appID value above) or referenced by name (e.g. authority=authority, where the first authority is the parameter name and the second is the variable name to be passed to that parameter – yes, I made that more confusing than it needed to be).  Overall, pretty reasonable.

So if you were to run that (with your own tenant, app ID, and app secret details) you would get an Azure AD bearer token that can then be used for subsequent Microsoft Graph calls.

OK, so it’s then time to make a Graph call.  After some trial and error, I ended up with this structure:


So I added a few more modules (installed via pip if they aren’t already present on your device):

  • requests, used to make REST calls.
  • json, used to handle JSON objects (e.g. the results from the REST calls).
  • pandas, the Python Data Analysis Library used (in my particular case) for displaying tabular data (it can do lots more).

That enables me to do things like this:


Remember before when I mentioned the starting (working) directory?  I specified that because the “” module (which is just a single file) is located in that directory, so I can then do an “import pyAutopilot” without having to place the file in a module folder that is in the path.  (Just a laziness item in this case.) 

So I first called the authenticate method to get a bearer token, then called the devices method to get a list of all the Autopilot devices in my tenant.  Those are then displayed in a table.  If I wanted the JSON I could get that too:


Note that you could, if you granted sufficient rights to the app, query any Azure AD or Intune objects using the query function – nothing in this is specific to Autopilot.  Here’s an example to get all the Azure AD users (which required granting User.ReadWrite.All, or really just User.Read.All in this case, to the app, then re-authenticating to get a new bearer token):


Now there are still lots of things that could be done in this Python module, including support for paging of results (I don’t have enough devices in my tenant to run into that), adding objects, modifying existing objects, proper error handling, etc.  But that’s enough learning for the day.

I’ve attached the Python module if you are interested in trying it out or modifying it (provided as-is).


Categories: Microsoft Graph

4 replies »

  1. I have been following these posts for some time and very grateful for the work done here! I tried to add a comment to a more relevant post, but it looks like comments are closed after a certain amount of time. Hoping someone can assist with an Autopilot issue I cannot seem to get around:

    Deployment Background:

    User-driven Hybrid Azure AD Joined (testing VPN now). A single Azure AD device group where imported hardware IDs are manually assigned. This group is also used for the deployment profile, CSP, configuration profiles, and apps. The AD Join configuration profile is set to “all devices”.

    What works:

    When I import a hardware ID from scratch and assign to the group, everything works as expected.

    What doesn’t:

    It’s when I reset the device (from local device or “Fresh Start” in InTune), trouble starts to occur. No errors, but no CSP. None of my configuration profiles are pushed down to the device (such as VPN profile). After searching the internet, it seemed a complete reimport was the only way to reliably get it working again.

    To be as concise as possible, I noticed when enrolling the device for the first time, the enrolled InTune object is automatically added to the device group where the autopilot object is assigned. However on subsequent resets, the new InTune object (with new device name) does not, which makes sense why the CSP, configuration profiles, and apps do not get pushed. If I manually add it to the group, everything gets applied, but this “workaround” is not practical when in production. Is this expected behavior? Did I miss something in my configuration?

    Any advice is greatly appreciated. Thank you!


  2. Michael, thank you so much for your reply. After putting a fresh set of eyes on it, I believe it is resolved.

    It turned out to be an artifact object from a previous testing session that needed to be cleaned up. Due to my deployment being in a testing phase (wiping test machines several times), I likely lost track when I went back to tidy up. Your article/script helped me A LOT with identifying the orphaned objects:

    The referenced article has by far the best visualization of the hybrid AD join and redeployment process. I imagine something like this will be very helpful after 1000+ objects and several engineering “hands in the cookie jar”. 🙂

    Thank you for all that you have contributed!