While it is possible to create templates without a good understanding of scope, it may be helpful to understand how liquid variables are scoped in order to create truly reusable template or integrate smoothly with code written by other developers.
Put as simply as possible, scope is the context in which any particular object is accessible.
Scopes may be nested similarly to how partial templates may be nested - with a root scope which may contain child scopes, each of which may also contain child scopes.
Any object on a parent scope is accessible in all child scopes unless the child scope has another object by the same name.
Object on a child scope, however, are no longer accessible on the parent scope once the child scope is complete.
Additionally, if an object exists by the same name on both a parent and a child scope, only the object on the child scope will be accessible until the child scope is complete.
Child scopes are created every time you open up a new block tag (eg: {% if %}, {% for %}, {% capture %}, etc...) or include a partial template. The child scope is completed when the block tag is closed (eg: {% endif %}, {% endfor %}, {% endcapture %}, etc...) or the partial template is finished rendering.
The liquid developer documentation should indicate which tags create child scopes (eg: "The case tag creates a new child scope." in the {% case %} documentation).
On the most basic level, if you save objects on a child scope, they will not be available when the child scope is completed. This means that if you attempt to retrieve a value inside an {% if %} statement, it will be unavailable after the matching {% endif %} tag unless you save that value on a parent scope.
On a more advanced level, if you are creating reusable templates that you want to allow other people to use without messing up their templates while still using readable variable names, all you need to do is be careful to save your objects on the child scope and when your template is done rendering it will not have disturbed any of the parent template objects or variables. For example: you could use the variable "start" even if the same variable is used by a parent template, and when your template is done rendering the "start" object would be unchanged inside its scope.
Ahh. Now we get to the point of this discussion. There are three different ways to save objects on the scope which are designed to give you full control over where objects are saved without becoming overly complicated:
This is the default functionality for all but the {% assign %} and {% set %} tags. Objects saved using "var" functionality are placed on the current scope and will be unavilable once that scope is completed. See the {% var %} tag for more details and examples.
Objects saved using "set" functionality will save the value on the nearest scope that already has another object by the same name. If there are no scopes that have an object by the same name, the object will be saved on the root scope. See the {% set %} tag for more details and examples.
Objects saved using the "assign" functionality will save the value on the root scope, and additionally remove all objects by the same name from any child scopes. This guarantees that the object is immediately available in all scopes. It is possible to create entire sites and use only the assign functionality. If you are developing reusable partial templates, however, the danger of using assign is that a naming collision could potentially prevent a parent template from functioning the way it was intended. See the {% assign %} tag for more details and examples.